From d03f8904715d2b186c20ada9d792f6788644e130 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 21 Oct 2024 20:36:00 +0530 Subject: [PATCH] feat: completed new user detail page screen --- backend/src/ee/routes/v1/index.ts | 2 +- ...oject-user-additional-privilege-service.ts | 9 +- frontend/src/components/v2/Alert/Alert.tsx | 2 +- .../mutation.tsx | 6 +- .../queries.tsx | 6 +- .../projectUserAdditionalPrivilege/types.tsx | 25 +- frontend/src/hooks/api/roles/queries.tsx | 8 +- frontend/src/hooks/api/workspace/index.tsx | 5 +- .../[id]/members/[membershipId]/index.tsx | 1 + .../MemberDetailsPage/MemberDetailPage.tsx | 108 +++-- ...emberProjectAdditionalPrivilegeSection.tsx | 251 ++++++++++++ ...rojectAdditionalPrivilegeModifySection.tsx | 387 ++++++++++++++++++ .../index.tsx | 1 + .../MemberRoleDetailsSection.tsx | 44 +- .../MemberRoleForm/MemberRoleForm.tsx | 21 +- .../MembersTab/components/MembersTable.tsx | 2 +- .../src/views/Project/RolePage/RolePage.tsx | 2 +- .../Project/RolePage/components/RoleModal.tsx | 13 +- ....ts => ProjectRoleModifySection.utils.tsx} | 4 +- .../RolePermissionsSection.tsx | 16 +- .../components/NewPermissionRule.tsx | 6 +- 21 files changed, 809 insertions(+), 110 deletions(-) create mode 100644 frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/index.tsx rename frontend/src/views/Project/RolePage/components/RolePermissionsSection/{ProjectRoleModifySection.utils.ts => ProjectRoleModifySection.utils.tsx} (99%) diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 961e06949..1bf896a85 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -81,9 +81,9 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register(registerSecretVersionRouter, { prefix: "/secret" }); await server.register(registerGroupRouter, { prefix: "/groups" }); await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" }); + await server.register(registerUserAdditionalPrivilegeRouter, { prefix: "/user-project-additional-privilege" }); await server.register( async (privilegeRouter) => { - await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" }); await privilegeRouter.register(registerIdentityProjectAdditionalPrivilegeRouter, { prefix: "/identity" }); }, { prefix: "/additional-privilege" } diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index 64742364d..a2cd7909d 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -145,7 +145,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime; const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange; const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { - ...dto, + slug: dto.slug, + permissions: dto.permissions, + isTemporary: dto.isTemporary, + temporaryRange: dto.temporaryRange, + temporaryMode: dto.temporaryMode, temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); @@ -157,7 +161,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ } const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { - ...dto, + slug: dto.slug, + permissions: dto.permissions, isTemporary: false, temporaryAccessStartTime: null, temporaryAccessEndTime: null, diff --git a/frontend/src/components/v2/Alert/Alert.tsx b/frontend/src/components/v2/Alert/Alert.tsx index 3d2dd6393..d090925f2 100644 --- a/frontend/src/components/v2/Alert/Alert.tsx +++ b/frontend/src/components/v2/Alert/Alert.tsx @@ -9,7 +9,7 @@ import { type VariantProps, cva } from "cva"; import { twMerge } from "tailwind-merge"; const alertVariants = cva( - "w-full bg-mineshaft-800 rounded-lg border px-4 py-3 text-sm flex items-center gap-x-4", + "w-full bg-mineshaft-800 rounded-lg border border-bunker-400 px-4 py-3 text-sm flex items-center gap-x-4", { variants: { variant: { diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx index 2ee6519f6..de0a1a51e 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx @@ -15,7 +15,7 @@ export const useCreateProjectUserAdditionalPrivilege = () => { return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({ mutationFn: async (dto) => { - const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", dto); + const { data } = await apiRequest.post("/api/v1/user-project-additional-privilege", dto); return data.privilege; }, onSuccess: (_, { projectMembershipId }) => { @@ -30,7 +30,7 @@ export const useUpdateProjectUserAdditionalPrivilege = () => { return useMutation<{ privilege: TProjectUserPrivilege }, {}, TUpdateProjectUserPrivlegeDTO>({ mutationFn: async (dto) => { const { data } = await apiRequest.patch( - `/api/v1/additional-privilege/users/${dto.privilegeId}`, + `/api/v1/user-project-additional-privilege/${dto.privilegeId}`, dto ); return data.privilege; @@ -47,7 +47,7 @@ export const useDeleteProjectUserAdditionalPrivilege = () => { return useMutation<{ privilege: TProjectUserPrivilege }, {}, TDeleteProjectUserPrivilegeDTO>({ mutationFn: async (dto) => { const { data } = await apiRequest.delete( - `/api/v1/additional-privilege/users/${dto.privilegeId}` + `/api/v1/user-project-additional-privilege/${dto.privilegeId}` ); return data.privilege; }, diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx index 261821c1f..fd31015cf 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx @@ -15,8 +15,8 @@ const fetchProjectUserPrivilegeDetails = async (privilegeId: string) => { const { data: { privilege } } = await apiRequest.get<{ - privilege: Omit & { permissions: unknown }; - }>(`/api/v1/additional-privilege/users/${privilegeId}`); + privilege: Omit & { permissions: TProjectPermission[] }; + }>(`/api/v1/user-project-additional-privilege/${privilegeId}`); return privilege; }; @@ -37,7 +37,7 @@ export const useListProjectUserPrivileges = (projectMembershipId: string) => { data: { privileges } } = await apiRequest.get<{ privileges: Array & { permissions: unknown }>; - }>("/api/v1/additional-privilege/users", { params: { projectMembershipId } }); + }>("/api/v1/user-project-additional-privilege", { params: { projectMembershipId } }); return privileges.map((el) => ({ ...el, permissions: el.permissions as TProjectPermission[] diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx index da40005c2..c3faae369 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx @@ -4,15 +4,6 @@ export enum ProjectUserAdditionalPrivilegeTemporaryMode { Relative = "relative" } -export type TProjectSpecificPrivilegePermission = { - conditions: { - environment: string; - secretPath?: { $glob: string }; - }; - actions: string[]; - subject: string; -}; - export type TProjectUserPrivilege = { projectMembershipId: string; slug: string; @@ -40,11 +31,17 @@ export type TProjectUserPrivilege = { export type TCreateProjectUserPrivilegeDTO = { projectMembershipId: string; slug?: string; - isTemporary?: boolean; - temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode; - temporaryRange?: string; - temporaryAccessStartTime?: string; - permissions: TProjectSpecificPrivilegePermission; + type: + | { + isTemporary: true; + temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode; + temporaryRange?: string; + temporaryAccessStartTime?: string; + } + | { + isTemporary: false; + }; + permissions: TProjectPermission[]; }; export type TUpdateProjectUserPrivlegeDTO = { diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index ebd6266a4..d761aec0d 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -63,16 +63,16 @@ export const useGetProjectRoles = (projectId: string) => enabled: Boolean(projectId) }); -export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) => +export const useGetProjectRoleBySlug = (projectId: string, roleSlug: string) => useQuery({ - queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug), + queryKey: roleQueryKeys.getProjectRoleBySlug(projectId, roleSlug), queryFn: async () => { const { data } = await apiRequest.get<{ role: TProjectRole }>( - `/api/v2/workspace/${projectSlug}/roles/slug/${roleSlug}` + `/api/v2/workspace/${projectId}/roles/slug/${roleSlug}` ); return data.role; }, - enabled: Boolean(projectSlug && roleSlug) + enabled: Boolean(projectId && roleSlug) }); const getOrgRoles = async (orgId: string) => { diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index 4fdf1d131..bff2d7150 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -23,6 +23,7 @@ export { useGetWorkspaceIntegrations, useGetWorkspaceSecrets, useGetWorkspaceSlackConfig, + useGetWorkspaceUserDetails, useGetWorkspaceUsers, useListWorkspaceCas, useListWorkspaceCertificates, @@ -36,7 +37,5 @@ export { useUpdateIdentityWorkspaceRole, useUpdateUserWorkspaceRole, useUpdateWsEnvironment, - useUpgradeProject, - useGetWorkspaceUserDetails -} from "./queries"; + useUpgradeProject} from "./queries"; export { workspaceKeys } from "./query-keys"; diff --git a/frontend/src/pages/project/[id]/members/[membershipId]/index.tsx b/frontend/src/pages/project/[id]/members/[membershipId]/index.tsx index 5dfbe64bc..2ec4b482c 100644 --- a/frontend/src/pages/project/[id]/members/[membershipId]/index.tsx +++ b/frontend/src/pages/project/[id]/members/[membershipId]/index.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { useTranslation } from "react-i18next"; import Head from "next/head"; + import { MemberDetailsPage } from "@app/views/Project/MemberDetailsPage"; export default function Page() { diff --git a/frontend/src/views/Project/MemberDetailsPage/MemberDetailPage.tsx b/frontend/src/views/Project/MemberDetailsPage/MemberDetailPage.tsx index 258988d71..d28d03dda 100644 --- a/frontend/src/views/Project/MemberDetailsPage/MemberDetailPage.tsx +++ b/frontend/src/views/Project/MemberDetailsPage/MemberDetailPage.tsx @@ -1,11 +1,15 @@ -import { Button } from "@app/components/v2"; +import { useRouter } from "next/router"; +import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, EmptyState, Spinner } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { withProjectPermission } from "@app/hoc"; import { useGetWorkspaceUserDetails } from "@app/hooks/api"; -import { faChevronLeft, faUser } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { format } from "date-fns"; -import { useRouter } from "next/router"; + +import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection"; import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection"; export const MemberDetailsPage = withProjectPermission( @@ -19,6 +23,14 @@ export const MemberDetailsPage = withProjectPermission( const { data: membershipDetails, isLoading: isMembershipDetailsLoading } = useGetWorkspaceUserDetails(workspaceId, membershipId); + if (isMembershipDetailsLoading) { + return ( +
+ +
+ ); + } + return (
@@ -34,36 +46,66 @@ export const MemberDetailsPage = withProjectPermission( Project Access Control
-
-
-
-

Project User Management

-
- + {membershipDetails ? ( + <> +
+
+
+

Project User Access

+
+ + {(isAllowed) => ( + + )} + +
+
+
+
+ Name + {membershipDetails && ( +

+ {membershipDetails.user.firstName || membershipDetails.user.lastName + ? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}` + : "-"} +

+ )} +
+
+ Email + {membershipDetails && ( +

{membershipDetails?.user?.email}

+ )} +
+
+
+ Joined on{" "} + {membershipDetails?.createdAt && + format(new Date(membershipDetails?.createdAt || ""), "yyyy-MM-dd")} +
-
- {membershipDetails && ( -

- {membershipDetails.user.firstName || membershipDetails.user.lastName - ? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}` - : "-"} -

- )} -
- Joined on{" "} - {membershipDetails?.createdAt && - format(new Date(membershipDetails?.createdAt || ""), "yyyy-MM-dd")} -
-
-
-
- + + + + ) : ( + + )}
); }, diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MemberProjectAdditionalPrivilegeSection.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MemberProjectAdditionalPrivilegeSection.tsx index e69de29bb..53a9e4ec5 100644 --- a/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MemberProjectAdditionalPrivilegeSection.tsx +++ b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MemberProjectAdditionalPrivilegeSection.tsx @@ -0,0 +1,251 @@ +import { faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format,formatDistance } from "date-fns"; +import { AnimatePresence, motion } from "framer-motion"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + DeleteActionModal, + EmptyState, + IconButton, + Table, + TableContainer, + TableSkeleton, + Tag, + TBody, + Td, + Th, + THead, + Tooltip, + Tr} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useUser +} from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { + useDeleteProjectUserAdditionalPrivilege, + useListProjectUserPrivileges +} from "@app/hooks/api"; +import { TWorkspaceUser } from "@app/hooks/api/types"; + +import { MembershipProjectAdditionalPrivilegeModifySection } from "./MembershipProjectAdditionalPrivilegeModifySection"; + +type Props = { + membershipDetails: TWorkspaceUser; +}; + +export const MemberProjectAdditionalPrivilegeSection = ({ membershipDetails }: Props) => { + const { user } = useUser(); + const userId = user?.id; + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "deletePrivilege", + "modifyPrivilege" + ] as const); + const { permission } = useProjectPermission(); + + const { mutateAsync: deletePrivilege } = useDeleteProjectUserAdditionalPrivilege(); + + const { data: userProjectPrivileges, isLoading } = useListProjectUserPrivileges( + membershipDetails?.id + ); + + const isOwnProjectMembershipDetails = userId === membershipDetails?.user?.id; + + const handlePrivilegeDelete = async () => { + const { id } = popUp?.deletePrivilege?.data as { id: string }; + try { + await deletePrivilege({ + privilegeId: id, + projectMembershipId: membershipDetails.id + }); + createNotification({ type: "success", text: "Successfully removed the privilege" }); + handlePopUpClose("deletePrivilege"); + } catch (err) { + console.log(err); + createNotification({ type: "error", text: "Failed to delete role" }); + } + }; + + return ( +
+ + {popUp?.modifyPrivilege.isOpen ? ( + + handlePopUpClose("modifyPrivilege")} + projectMembershipId={membershipDetails?.id} + privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id} + isDisabled={ + isOwnProjectMembershipDetails || + permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member) + } + /> + + ) : ( + +
+

+ Project Additional Privilege +

+ {userId !== membershipDetails?.user?.id && + membershipDetails?.status !== "invited" && ( + + {(isAllowed) => ( + { + handlePopUpOpen("modifyPrivilege"); + }} + isDisabled={!isAllowed} + > + + + )} + + )} +
+
+ + + + + + + + + + {isLoading && } + {!isLoading && + userProjectPrivileges?.map((privilegeDetails) => { + const isTemporary = privilegeDetails?.isTemporary; + const isExpired = + privilegeDetails.isTemporary && + new Date() > new Date(privilegeDetails.temporaryAccessEndTime || ""); + + let text = "Permanent"; + let toolTipText = "Non expiry access"; + if (privilegeDetails.isTemporary) { + if (isExpired) { + text = "Access Expired"; + toolTipText = "Timed Access Expired"; + } else { + text = formatDistance( + new Date(privilegeDetails.temporaryAccessEndTime || ""), + new Date() + ); + toolTipText = `Until ${format( + new Date(privilegeDetails.temporaryAccessEndTime || ""), + "yyyy-MM-dd hh:mm:ss aaa" + )}`; + } + } + + return ( + { + if (evt.key === "Enter") { + handlePopUpOpen("modifyPrivilege", privilegeDetails); + } + }} + onClick={() => handlePopUpOpen("modifyPrivilege", privilegeDetails)} + > + + + + + ); + })} + +
NameType +
{privilegeDetails.slug} + + + {text} + + + +
+ + {(isAllowed) => ( + { + e.stopPropagation(); + e.preventDefault(); + handlePopUpOpen("deletePrivilege", { + id: privilegeDetails?.id, + slug: privilegeDetails?.slug + }); + }} + > + + + )} + +
+
+ {!isLoading && !userProjectPrivileges?.length && ( + + )} +
+
+ handlePopUpToggle("deletePrivilege", isOpen)} + onDeleteApproved={() => handlePrivilegeDelete()} + /> +
+ )} +
+
+ ); +}; diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index e69de29bb..5f7407976 100644 --- a/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -0,0 +1,387 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { + faCaretDown, + faChevronLeft, + faClock, + faPlus, + faSave +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format, formatDistance } from "date-fns"; +import ms from "ms"; +import { twMerge } from "tailwind-merge"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + IconButton, + Input, + Modal, + ModalContent, + ModalTrigger, + Popover, + PopoverContent, + PopoverTrigger, + Tag, + Tooltip +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { + useCreateProjectUserAdditionalPrivilege, + useGetProjectUserPrivilegeDetails, + useUpdateProjectUserAdditionalPrivilege +} from "@app/hooks/api"; +import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types"; +import { GeneralPermissionPolicies } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies"; +import { NewPermissionRule } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/NewPermissionRule"; +import { PermissionEmptyState } from "@app/views/Project/RolePage/components/RolePermissionsSection/PermissionEmptyState"; +import { + formRolePermission2API, + PROJECT_PERMISSION_OBJECT, + projectRoleFormSchema, + rolePermission2Form +} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils"; +import { renderConditionalComponents } from "@app/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection"; + +type Props = { + privilegeId?: string; + projectMembershipId: string; + onGoBack: () => void; + isDisabled?: boolean; +}; + +export const formSchema = z.object({ + slug: z.string().optional(), + temporaryAccess: z + .discriminatedUnion("isTemporary", [ + z.object({ + isTemporary: z.literal(true), + temporaryRange: z.string().min(1), + temporaryAccessStartTime: z.string().datetime(), + temporaryAccessEndTime: z.string().datetime().nullable().optional() + }), + z.object({ + isTemporary: z.literal(false) + }) + ]) + .default({ isTemporary: false }), + permissions: projectRoleFormSchema.shape.permissions +}); + +type TFormSchema = z.infer; + +export const MembershipProjectAdditionalPrivilegeModifySection = ({ + privilegeId, + onGoBack, + projectMembershipId, + isDisabled +}: Props) => { + const isCreate = !privilegeId; + const { currentWorkspace } = useWorkspace(); + const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); + const projectId = currentWorkspace?.id || ""; + const { data: privilegeDetails, isLoading } = useGetProjectUserPrivilegeDetails( + privilegeId || "" + ); + const { permission } = useProjectPermission(); + const isMemberEditDisabled = permission.cannot( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Member + ); + + const form = useForm({ + values: privilegeDetails + ? { + ...privilegeDetails, + permissions: rolePermission2Form(privilegeDetails.permissions), + temporaryAccess: privilegeDetails.isTemporary + ? { + isTemporary: true, + temporaryRange: privilegeDetails.temporaryRange || "", + temporaryAccessEndTime: privilegeDetails.temporaryAccessEndTime || "", + temporaryAccessStartTime: privilegeDetails.temporaryAccessStartTime || "" + } + : { + isTemporary: privilegeDetails.isTemporary + } + } + : undefined, + resolver: zodResolver(formSchema) + }); + + const { + handleSubmit, + formState: { isDirty, isSubmitting } + } = form; + + const { mutateAsync: updateUserProjectAdditionalPrivilege } = + useUpdateProjectUserAdditionalPrivilege(); + const { mutateAsync: createUserProjectAdditionalPrivilege } = + useCreateProjectUserAdditionalPrivilege(); + + const onSubmit = async (el: TFormSchema) => { + const accessType = !el.temporaryAccess.isTemporary + ? { role: el.slug, isTemporary: false as const } + : { + role: el.slug, + isTemporary: true as const, + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: el.temporaryAccess.temporaryRange, + temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime + }; + + try { + if (isCreate) { + await createUserProjectAdditionalPrivilege({ + permissions: formRolePermission2API(el.permissions), + projectMembershipId, + slug: el.slug || undefined, + type: accessType + }); + createNotification({ type: "success", text: "Successfully created privilege" }); + } else { + if (!projectId || !privilegeDetails?.id) return; + await updateUserProjectAdditionalPrivilege({ + privilegeId: privilegeDetails.id, + permissions: formRolePermission2API(el.permissions), + projectMembershipId, + slug: el.slug || undefined, + type: accessType + }); + createNotification({ type: "success", text: "Successfully updated privilege" }); + } + onGoBack(); + } catch (err) { + console.log(err); + createNotification({ type: "error", text: "Failed to update role" }); + } + }; + + const privilegeTemporaryAccess = form.watch("temporaryAccess"); + const isTemporary = privilegeTemporaryAccess?.isTemporary; + const isExpired = + privilegeTemporaryAccess?.isTemporary && + new Date() > new Date(privilegeTemporaryAccess.temporaryAccessEndTime || ""); + let text = "Permanent"; + let toolTipText = "Non expiry access"; + + if (isTemporary) { + if (isExpired) { + text = "Access Expired"; + toolTipText = "Timed Access Expired"; + } else { + text = formatDistance( + new Date(privilegeTemporaryAccess.temporaryAccessEndTime || ""), + new Date() + ); + toolTipText = `Until ${format( + new Date(privilegeTemporaryAccess.temporaryAccessEndTime || ""), + "yyyy-MM-dd hh:mm:ss aaa" + )}`; + } + } + + return ( +
+ +
+
+ + + +

+ Modify Additional Privilege +

+
+
+ {isDirty && ( + + )} +
+ + handlePopUpToggle("createPolicy", isOpen)} + > + + + + + handlePopUpToggle("createPolicy")} /> + + +
+
+
+
+
Overview
+
+
+ ( + + + + )} + /> +
+
+ + +
+ + + +
+
+ +
+
+ Configure timed access +
+ {isExpired && Expired} + ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+ + {isTemporary && ( + + )} +
+
+
+
+
+
+
+
+
Policies
+ {(isCreate || !isLoading) && } + {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( + + {renderConditionalComponents(subject, isDisabled)} + + ))} +
+
+
+ ); +}; diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/index.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/index.tsx new file mode 100644 index 000000000..c563ab035 --- /dev/null +++ b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/index.tsx @@ -0,0 +1 @@ +export { MemberProjectAdditionalPrivilegeSection } from "./MemberProjectAdditionalPrivilegeSection"; diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx index 3f498aa8b..cca4d188e 100644 --- a/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx +++ b/frontend/src/views/Project/MemberDetailsPage/components/MemberRoleDetailsSection/MemberRoleDetailsSection.tsx @@ -1,22 +1,26 @@ +import { faFolder, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format, formatDistance } from "date-fns"; +import { twMerge } from "tailwind-merge"; + import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { DeleteActionModal, - IconButton, - TableContainer, - Td, - Table, - Tr, - Th, - THead, - TableSkeleton, EmptyState, - TBody, - Tooltip, + IconButton, Modal, ModalContent, - Tag -} from "@app/components/v2"; + Table, + TableContainer, + TableSkeleton, + Tag, + TBody, + Td, + Th, + THead, + Tooltip, + Tr} from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, @@ -28,10 +32,7 @@ import { usePopUp } from "@app/hooks"; import { useUpdateUserWorkspaceRole } from "@app/hooks/api"; import { TProjectRole } from "@app/hooks/api/roles/types"; import { TWorkspaceUser } from "@app/hooks/api/types"; -import { faFolder, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { format, formatDistance } from "date-fns"; -import { twMerge } from "tailwind-merge"; + import { MemberRoleModify } from "./MemberRoleModify"; type Props = { @@ -44,6 +45,7 @@ export const MemberRoleDetailsSection = ({ isMembershipDetailsLoading }: Props) => { const { user } = useUser(); + const userId = user?.id; const { currentWorkspace } = useWorkspace(); const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ "deleteRole", @@ -51,7 +53,7 @@ export const MemberRoleDetailsSection = ({ ] as const); const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole(); - const userId = user?.id; + const isOwnProjectMembershipDetails = userId === membershipDetails?.user?.id; const handleRoleDelete = async () => { const { id } = popUp?.deleteRole?.data as TProjectRole; @@ -71,10 +73,10 @@ export const MemberRoleDetailsSection = ({ }; return ( -
+

Project Roles

- {userId !== membershipDetails?.user?.id && membershipDetails?.status !== "invited" && ( + {!isOwnProjectMembershipDetails && membershipDetails?.status !== "invited" && ( { e.stopPropagation(); handlePopUpOpen("deleteRole", { @@ -191,7 +193,7 @@ export const MemberRoleDetailsSection = ({ {!isMembershipDetailsLoading && !membershipDetails?.roles?.length && ( - + )}
diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRoleForm.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRoleForm.tsx index da6243281..7b8aaf92d 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRoleForm.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/MemberRoleForm.tsx @@ -1,20 +1,29 @@ +import Link from "next/link"; + +import { Alert, AlertDescription } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; import { TWorkspaceUser } from "@app/hooks/api/types"; import { MemberRbacSection } from "./MemberRbacSection"; -import { SpecificPrivilegeSection } from "./SpecificPrivilegeSection"; type Props = { projectMember: TWorkspaceUser; onOpenUpgradeModal: (title: string) => void; }; export const MemberRoleForm = ({ projectMember, onOpenUpgradeModal }: Props) => { + const { currentWorkspace } = useWorkspace(); return (
- - + + + + Additional privileges now offer full permissions and have been moved to a new screen. +
+ + Click here to access them. + +
+
); }; diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx index ad4fe490c..a8d9fcfc7 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MembersTable.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from "react"; +import { useRouter } from "next/router"; import { faClock, faEdit, @@ -7,7 +8,6 @@ import { faUsers } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useRouter } from "next/router"; import { twMerge } from "tailwind-merge"; import { ProjectPermissionCan } from "@app/components/permissions"; diff --git a/frontend/src/views/Project/RolePage/RolePage.tsx b/frontend/src/views/Project/RolePage/RolePage.tsx index 929265dcf..482f52d2e 100644 --- a/frontend/src/views/Project/RolePage/RolePage.tsx +++ b/frontend/src/views/Project/RolePage/RolePage.tsx @@ -44,7 +44,7 @@ export const RolePage = withProjectPermission( if (!currentWorkspace?.slug || !data?.id) return; await deleteProjectRole({ - projectSlug: currentWorkspace.slug, + projectId, id: data.id }); diff --git a/frontend/src/views/Project/RolePage/components/RoleModal.tsx b/frontend/src/views/Project/RolePage/components/RoleModal.tsx index 435378a02..5a87b4a61 100644 --- a/frontend/src/views/Project/RolePage/components/RoleModal.tsx +++ b/frontend/src/views/Project/RolePage/components/RoleModal.tsx @@ -10,7 +10,8 @@ import { useWorkspace } from "@app/context"; import { useCreateProjectRole, useGetProjectRoleBySlug, - useUpdateProjectRole} from "@app/hooks/api"; + useUpdateProjectRole +} from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = z @@ -36,9 +37,9 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { }; const { currentWorkspace } = useWorkspace(); - const projectSlug = currentWorkspace?.slug || ""; + const projectId = currentWorkspace?.id || ""; - const { data: role } = useGetProjectRoleBySlug(projectSlug, popupData?.roleSlug ?? ""); + const { data: role } = useGetProjectRoleBySlug(projectId, popupData?.roleSlug ?? ""); const { mutateAsync: createProjectRole } = useCreateProjectRole(); const { mutateAsync: updateProjectRole } = useUpdateProjectRole(); @@ -74,13 +75,13 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { const onFormSubmit = async ({ name, description, slug }: FormData) => { try { - if (!projectSlug) return; + if (!projectId) return; if (role) { // update await updateProjectRole({ id: role.id, - projectSlug, + projectId, name, description, slug @@ -90,7 +91,7 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => { } else { // create const newRole = await createProjectRole({ - projectSlug, + projectId, name, description, slug, diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.tsx similarity index 99% rename from frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts rename to frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.tsx index 5832e5a14..d717162d3 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.tsx @@ -71,7 +71,7 @@ const ConditionSchema = z { message: "Duplicate operator found for a condition" } ); -export const formSchema = z.object({ +export const projectRoleFormSchema = z.object({ name: z.string().trim(), description: z.string().trim().optional(), slug: z @@ -133,7 +133,7 @@ export const formSchema = z.object({ .optional() }); -export type TFormSchema = z.infer; +export type TFormSchema = z.infer; type TConditionalFields = | ProjectPermissionSub.Secrets diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index a7ac06010..2cd7ca3b4 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -17,9 +17,9 @@ import { SecretPermissionConditions } from "./components/SecretPermissionConditi import { PermissionEmptyState } from "./PermissionEmptyState"; import { formRolePermission2API, - formSchema, isConditionalSubjects, PROJECT_PERMISSION_OBJECT, + projectRoleFormSchema, rolePermission2Form, TFormSchema } from "./ProjectRoleModifySection.utils"; @@ -29,7 +29,10 @@ type Props = { isDisabled?: boolean; }; -const renderConditionalComponents = (subject: ProjectPermissionSub, isDisabled?: boolean) => { +export const renderConditionalComponents = ( + subject: ProjectPermissionSub, + isDisabled?: boolean +) => { if (subject === ProjectPermissionSub.Secrets) return ; @@ -43,7 +46,7 @@ const renderConditionalComponents = (subject: ProjectPermissionSub, isDisabled?: export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { const { currentWorkspace } = useWorkspace(); const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); - const projectSlug = currentWorkspace?.slug || ""; + const projectId = currentWorkspace?.id || ""; const { data: role, isLoading } = useGetProjectRoleBySlug( currentWorkspace?.slug ?? "", roleSlug as string @@ -51,7 +54,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { const form = useForm({ values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined, - resolver: zodResolver(formSchema) + resolver: zodResolver(projectRoleFormSchema) }); const { @@ -64,14 +67,13 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { const onSubmit = async (el: TFormSchema) => { try { - if (!projectSlug || !role?.id) return; + if (!projectId || !role?.id) return; await updateRole({ id: role?.id as string, - projectSlug, + projectId, ...el, permissions: formRolePermission2API(el.permissions) }); - createNotification({ type: "success", text: "Successfully updated role" }); } catch (err) { console.log(err); diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/NewPermissionRule.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/NewPermissionRule.tsx index 710b82cb3..91008e247 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/NewPermissionRule.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/NewPermissionRule.tsx @@ -14,9 +14,9 @@ import { import { ProjectPermissionSub } from "@app/context"; import { - formSchema, isConditionalSubjects, PROJECT_PERMISSION_OBJECT, + projectRoleFormSchema, TFormSchema } from "../ProjectRoleModifySection.utils"; @@ -32,7 +32,9 @@ export const NewPermissionRule = ({ onClose }: Props) => { permissions: NonNullable; }>({ resolver: zodResolver( - formSchema.pick({ permissions: true }).extend({ type: z.nativeEnum(ProjectPermissionSub) }) + projectRoleFormSchema + .pick({ permissions: true }) + .extend({ type: z.nativeEnum(ProjectPermissionSub) }) ), defaultValues: { type: ProjectPermissionSub.Secrets