mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 13:28:27 +00:00
feat: completed new user detail page screen
This commit is contained in:
@@ -81,9 +81,9 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||||
await server.register(registerGroupRouter, { prefix: "/groups" });
|
await server.register(registerGroupRouter, { prefix: "/groups" });
|
||||||
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
|
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
|
||||||
|
await server.register(registerUserAdditionalPrivilegeRouter, { prefix: "/user-project-additional-privilege" });
|
||||||
await server.register(
|
await server.register(
|
||||||
async (privilegeRouter) => {
|
async (privilegeRouter) => {
|
||||||
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
|
|
||||||
await privilegeRouter.register(registerIdentityProjectAdditionalPrivilegeRouter, { prefix: "/identity" });
|
await privilegeRouter.register(registerIdentityProjectAdditionalPrivilegeRouter, { prefix: "/identity" });
|
||||||
},
|
},
|
||||||
{ prefix: "/additional-privilege" }
|
{ prefix: "/additional-privilege" }
|
||||||
|
|||||||
+7
-2
@@ -145,7 +145,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime;
|
const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime;
|
||||||
const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange;
|
const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange;
|
||||||
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
|
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 || ""),
|
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
||||||
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
||||||
});
|
});
|
||||||
@@ -157,7 +161,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
|
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
|
||||||
...dto,
|
slug: dto.slug,
|
||||||
|
permissions: dto.permissions,
|
||||||
isTemporary: false,
|
isTemporary: false,
|
||||||
temporaryAccessStartTime: null,
|
temporaryAccessStartTime: null,
|
||||||
temporaryAccessEndTime: null,
|
temporaryAccessEndTime: null,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { type VariantProps, cva } from "cva";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
const alertVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const useCreateProjectUserAdditionalPrivilege = () => {
|
|||||||
|
|
||||||
return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({
|
return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({
|
||||||
mutationFn: async (dto) => {
|
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;
|
return data.privilege;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { projectMembershipId }) => {
|
onSuccess: (_, { projectMembershipId }) => {
|
||||||
@@ -30,7 +30,7 @@ export const useUpdateProjectUserAdditionalPrivilege = () => {
|
|||||||
return useMutation<{ privilege: TProjectUserPrivilege }, {}, TUpdateProjectUserPrivlegeDTO>({
|
return useMutation<{ privilege: TProjectUserPrivilege }, {}, TUpdateProjectUserPrivlegeDTO>({
|
||||||
mutationFn: async (dto) => {
|
mutationFn: async (dto) => {
|
||||||
const { data } = await apiRequest.patch(
|
const { data } = await apiRequest.patch(
|
||||||
`/api/v1/additional-privilege/users/${dto.privilegeId}`,
|
`/api/v1/user-project-additional-privilege/${dto.privilegeId}`,
|
||||||
dto
|
dto
|
||||||
);
|
);
|
||||||
return data.privilege;
|
return data.privilege;
|
||||||
@@ -47,7 +47,7 @@ export const useDeleteProjectUserAdditionalPrivilege = () => {
|
|||||||
return useMutation<{ privilege: TProjectUserPrivilege }, {}, TDeleteProjectUserPrivilegeDTO>({
|
return useMutation<{ privilege: TProjectUserPrivilege }, {}, TDeleteProjectUserPrivilegeDTO>({
|
||||||
mutationFn: async (dto) => {
|
mutationFn: async (dto) => {
|
||||||
const { data } = await apiRequest.delete(
|
const { data } = await apiRequest.delete(
|
||||||
`/api/v1/additional-privilege/users/${dto.privilegeId}`
|
`/api/v1/user-project-additional-privilege/${dto.privilegeId}`
|
||||||
);
|
);
|
||||||
return data.privilege;
|
return data.privilege;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const fetchProjectUserPrivilegeDetails = async (privilegeId: string) => {
|
|||||||
const {
|
const {
|
||||||
data: { privilege }
|
data: { privilege }
|
||||||
} = await apiRequest.get<{
|
} = await apiRequest.get<{
|
||||||
privilege: Omit<TProjectUserPrivilege, "permissions"> & { permissions: unknown };
|
privilege: Omit<TProjectUserPrivilege, "permissions"> & { permissions: TProjectPermission[] };
|
||||||
}>(`/api/v1/additional-privilege/users/${privilegeId}`);
|
}>(`/api/v1/user-project-additional-privilege/${privilegeId}`);
|
||||||
return privilege;
|
return privilege;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ export const useListProjectUserPrivileges = (projectMembershipId: string) => {
|
|||||||
data: { privileges }
|
data: { privileges }
|
||||||
} = await apiRequest.get<{
|
} = await apiRequest.get<{
|
||||||
privileges: Array<Omit<TProjectUserPrivilege, "permissions"> & { permissions: unknown }>;
|
privileges: Array<Omit<TProjectUserPrivilege, "permissions"> & { permissions: unknown }>;
|
||||||
}>("/api/v1/additional-privilege/users", { params: { projectMembershipId } });
|
}>("/api/v1/user-project-additional-privilege", { params: { projectMembershipId } });
|
||||||
return privileges.map((el) => ({
|
return privileges.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
permissions: el.permissions as TProjectPermission[]
|
permissions: el.permissions as TProjectPermission[]
|
||||||
|
|||||||
@@ -4,15 +4,6 @@ export enum ProjectUserAdditionalPrivilegeTemporaryMode {
|
|||||||
Relative = "relative"
|
Relative = "relative"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TProjectSpecificPrivilegePermission = {
|
|
||||||
conditions: {
|
|
||||||
environment: string;
|
|
||||||
secretPath?: { $glob: string };
|
|
||||||
};
|
|
||||||
actions: string[];
|
|
||||||
subject: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TProjectUserPrivilege = {
|
export type TProjectUserPrivilege = {
|
||||||
projectMembershipId: string;
|
projectMembershipId: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -40,11 +31,17 @@ export type TProjectUserPrivilege = {
|
|||||||
export type TCreateProjectUserPrivilegeDTO = {
|
export type TCreateProjectUserPrivilegeDTO = {
|
||||||
projectMembershipId: string;
|
projectMembershipId: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
isTemporary?: boolean;
|
type:
|
||||||
|
| {
|
||||||
|
isTemporary: true;
|
||||||
temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode;
|
temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode;
|
||||||
temporaryRange?: string;
|
temporaryRange?: string;
|
||||||
temporaryAccessStartTime?: string;
|
temporaryAccessStartTime?: string;
|
||||||
permissions: TProjectSpecificPrivilegePermission;
|
}
|
||||||
|
| {
|
||||||
|
isTemporary: false;
|
||||||
|
};
|
||||||
|
permissions: TProjectPermission[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateProjectUserPrivlegeDTO = {
|
export type TUpdateProjectUserPrivlegeDTO = {
|
||||||
|
|||||||
@@ -63,16 +63,16 @@ export const useGetProjectRoles = (projectId: string) =>
|
|||||||
enabled: Boolean(projectId)
|
enabled: Boolean(projectId)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) =>
|
export const useGetProjectRoleBySlug = (projectId: string, roleSlug: string) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug),
|
queryKey: roleQueryKeys.getProjectRoleBySlug(projectId, roleSlug),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{ role: TProjectRole }>(
|
const { data } = await apiRequest.get<{ role: TProjectRole }>(
|
||||||
`/api/v2/workspace/${projectSlug}/roles/slug/${roleSlug}`
|
`/api/v2/workspace/${projectId}/roles/slug/${roleSlug}`
|
||||||
);
|
);
|
||||||
return data.role;
|
return data.role;
|
||||||
},
|
},
|
||||||
enabled: Boolean(projectSlug && roleSlug)
|
enabled: Boolean(projectId && roleSlug)
|
||||||
});
|
});
|
||||||
|
|
||||||
const getOrgRoles = async (orgId: string) => {
|
const getOrgRoles = async (orgId: string) => {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export {
|
|||||||
useGetWorkspaceIntegrations,
|
useGetWorkspaceIntegrations,
|
||||||
useGetWorkspaceSecrets,
|
useGetWorkspaceSecrets,
|
||||||
useGetWorkspaceSlackConfig,
|
useGetWorkspaceSlackConfig,
|
||||||
|
useGetWorkspaceUserDetails,
|
||||||
useGetWorkspaceUsers,
|
useGetWorkspaceUsers,
|
||||||
useListWorkspaceCas,
|
useListWorkspaceCas,
|
||||||
useListWorkspaceCertificates,
|
useListWorkspaceCertificates,
|
||||||
@@ -36,7 +37,5 @@ export {
|
|||||||
useUpdateIdentityWorkspaceRole,
|
useUpdateIdentityWorkspaceRole,
|
||||||
useUpdateUserWorkspaceRole,
|
useUpdateUserWorkspaceRole,
|
||||||
useUpdateWsEnvironment,
|
useUpdateWsEnvironment,
|
||||||
useUpgradeProject,
|
useUpgradeProject} from "./queries";
|
||||||
useGetWorkspaceUserDetails
|
|
||||||
} from "./queries";
|
|
||||||
export { workspaceKeys } from "./query-keys";
|
export { workspaceKeys } from "./query-keys";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import { MemberDetailsPage } from "@app/views/Project/MemberDetailsPage";
|
import { MemberDetailsPage } from "@app/views/Project/MemberDetailsPage";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
|||||||
@@ -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 { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
import { withProjectPermission } from "@app/hoc";
|
||||||
import { useGetWorkspaceUserDetails } from "@app/hooks/api";
|
import { useGetWorkspaceUserDetails } from "@app/hooks/api";
|
||||||
import { faChevronLeft, faUser } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
|
||||||
import { format } from "date-fns";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
||||||
|
|
||||||
export const MemberDetailsPage = withProjectPermission(
|
export const MemberDetailsPage = withProjectPermission(
|
||||||
@@ -19,6 +23,14 @@ export const MemberDetailsPage = withProjectPermission(
|
|||||||
const { data: membershipDetails, isLoading: isMembershipDetailsLoading } =
|
const { data: membershipDetails, isLoading: isMembershipDetailsLoading } =
|
||||||
useGetWorkspaceUserDetails(workspaceId, membershipId);
|
useGetWorkspaceUserDetails(workspaceId, membershipId);
|
||||||
|
|
||||||
|
if (isMembershipDetailsLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center justify-center p-24">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 p-6 text-white">
|
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 p-6 text-white">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@@ -34,36 +46,66 @@ export const MemberDetailsPage = withProjectPermission(
|
|||||||
Project Access Control
|
Project Access Control
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{membershipDetails ? (
|
||||||
|
<>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex items-center justify-between ">
|
<div className="mb-4 flex items-center justify-between ">
|
||||||
<h3 className="text-lg font-semibold text-mineshaft-100">Project User Management</h3>
|
<h3 className="text-xl font-semibold text-mineshaft-100">Project User Access</h3>
|
||||||
<div>
|
<div>
|
||||||
<Button colorSchema="danger" variant="outline_bg" size="xs">
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Member}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Edit role"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
Remove User
|
Remove User
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="">
|
<div className="flex gap-12">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-semibold text-gray-400">Name</span>
|
||||||
{membershipDetails && (
|
{membershipDetails && (
|
||||||
<p className="capitalize">
|
<p className="text-lg capitalize">
|
||||||
{membershipDetails.user.firstName || membershipDetails.user.lastName
|
{membershipDetails.user.firstName || membershipDetails.user.lastName
|
||||||
? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
|
? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
|
||||||
: "-"}
|
: "-"}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="mt-2 text-sm text-gray-400">
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-semibold text-gray-400">Email</span>
|
||||||
|
{membershipDetails && (
|
||||||
|
<p className="text-lg">{membershipDetails?.user?.email}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 text-sm text-gray-400">
|
||||||
Joined on{" "}
|
Joined on{" "}
|
||||||
{membershipDetails?.createdAt &&
|
{membershipDetails?.createdAt &&
|
||||||
format(new Date(membershipDetails?.createdAt || ""), "yyyy-MM-dd")}
|
format(new Date(membershipDetails?.createdAt || ""), "yyyy-MM-dd")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<MemberRoleDetailsSection
|
<MemberRoleDetailsSection
|
||||||
membershipDetails={membershipDetails}
|
membershipDetails={membershipDetails}
|
||||||
isMembershipDetailsLoading={isMembershipDetailsLoading}
|
isMembershipDetailsLoading={isMembershipDetailsLoading}
|
||||||
/>
|
/>
|
||||||
|
<MemberProjectAdditionalPrivilegeSection membershipDetails={membershipDetails} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<EmptyState title="Error: Unable to find the user." className="py-12" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+251
@@ -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 (
|
||||||
|
<div className="relative">
|
||||||
|
<AnimatePresence>
|
||||||
|
{popUp?.modifyPrivilege.isOpen ? (
|
||||||
|
<motion.div
|
||||||
|
key="privilege-modify"
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
className="absolute min-h-[10rem] w-full"
|
||||||
|
>
|
||||||
|
<MembershipProjectAdditionalPrivilegeModifySection
|
||||||
|
onGoBack={() => handlePopUpClose("modifyPrivilege")}
|
||||||
|
projectMembershipId={membershipDetails?.id}
|
||||||
|
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||||
|
isDisabled={
|
||||||
|
isOwnProjectMembershipDetails ||
|
||||||
|
permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="privilege-list"
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
initial={{ opacity: 0, translateX: 0 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
className="absolute w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">
|
||||||
|
Project Additional Privilege
|
||||||
|
</h3>
|
||||||
|
{userId !== membershipDetails?.user?.id &&
|
||||||
|
membershipDetails?.status !== "invited" && (
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Member}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="New privilege"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("modifyPrivilege");
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="py-4">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Type</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={3} innerKey="user-project-memberships" />}
|
||||||
|
{!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 (
|
||||||
|
<Tr
|
||||||
|
key={`user-project-privilege-${privilegeDetails?.id}`}
|
||||||
|
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(evt) => {
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
handlePopUpOpen("modifyPrivilege", privilegeDetails);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => handlePopUpOpen("modifyPrivilege", privilegeDetails)}
|
||||||
|
>
|
||||||
|
<Td className="capitalize">{privilegeDetails.slug}</Td>
|
||||||
|
<Td>
|
||||||
|
<Tooltip asChild={false} content={toolTipText}>
|
||||||
|
<Tag
|
||||||
|
className={twMerge(
|
||||||
|
"capitalize",
|
||||||
|
isTemporary && "text-primary",
|
||||||
|
isExpired && "text-red-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Member}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Remove role"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
colorSchema="danger"
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
handlePopUpOpen("deletePrivilege", {
|
||||||
|
id: privilegeDetails?.id,
|
||||||
|
slug: privilegeDetails?.slug
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isLoading && !userProjectPrivileges?.length && (
|
||||||
|
<EmptyState title="This user has no additional privileges" icon={faFolder} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deletePrivilege.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title={`Do you want to remove role ${
|
||||||
|
(popUp?.deletePrivilege?.data as { slug: string; id: string })?.slug
|
||||||
|
}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deletePrivilege", isOpen)}
|
||||||
|
onDeleteApproved={() => handlePrivilegeDelete()}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+387
@@ -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<typeof formSchema>;
|
||||||
|
|
||||||
|
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<TFormSchema>({
|
||||||
|
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 (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
>
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<IconButton ariaLabel="go-back" variant="plain" onClick={onGoBack}>
|
||||||
|
<FontAwesomeIcon icon={faChevronLeft} />
|
||||||
|
</IconButton>
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">
|
||||||
|
Modify Additional Privilege
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{isDirty && (
|
||||||
|
<Button
|
||||||
|
className="mr-4 text-mineshaft-300"
|
||||||
|
variant="link"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
onClick={onGoBack}
|
||||||
|
>
|
||||||
|
Discard
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
type="submit"
|
||||||
|
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
|
||||||
|
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.createPolicy.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="h-10 rounded-l-none"
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
>
|
||||||
|
New policy
|
||||||
|
</Button>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent title="New Policy" subTitle="Policies grant additional permissions.">
|
||||||
|
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 border-b border-gray-800 p-4 first:rounded-t-md last:rounded-b-md">
|
||||||
|
<div className="mb-2 text-lg">Overview</div>
|
||||||
|
<div className=" flex items-end space-x-6">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="slug"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl label="Privilege Name" isOptional className="mb-0">
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger disabled={isMemberEditDisabled} asChild>
|
||||||
|
<div className="w-full max-w-md flex-grow">
|
||||||
|
<Tooltip content={toolTipText}>
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={isTemporary ? <FontAwesomeIcon icon={faClock} /> : undefined}
|
||||||
|
rightIcon={<FontAwesomeIcon icon={faCaretDown} className="ml-2" />}
|
||||||
|
isDisabled={isMemberEditDisabled}
|
||||||
|
className={twMerge(
|
||||||
|
"w-full border-none bg-mineshaft-600 py-2.5 text-xs capitalize hover:bg-mineshaft-500",
|
||||||
|
isTemporary && "text-primary",
|
||||||
|
isExpired && "text-red-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
arrowClassName="fill-gray-600"
|
||||||
|
side="right"
|
||||||
|
sideOffset={12}
|
||||||
|
hideCloseBtn
|
||||||
|
className="border border-gray-600 pt-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
|
||||||
|
Configure timed access
|
||||||
|
</div>
|
||||||
|
{isExpired && <Tag colorSchema="red">Expired</Tag>}
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
defaultValue="1h"
|
||||||
|
name="temporaryAccess.temporaryRange"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Validity" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
const temporaryRange = form.getValues("temporaryAccess.temporaryRange");
|
||||||
|
if (!temporaryRange) {
|
||||||
|
form.setError(
|
||||||
|
"temporaryAccess.temporaryRange",
|
||||||
|
{ type: "required", message: "Required" },
|
||||||
|
{ shouldFocus: true }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
form.clearErrors("temporaryAccess.temporaryRange");
|
||||||
|
form.setValue(
|
||||||
|
"temporaryAccess",
|
||||||
|
{
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryAccessStartTime: new Date().toISOString(),
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessEndTime: new Date(
|
||||||
|
new Date().getTime() + ms(temporaryRange)
|
||||||
|
).toISOString()
|
||||||
|
},
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isTemporary ? "Restart" : "Grant"}
|
||||||
|
</Button>
|
||||||
|
{isTemporary && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => {
|
||||||
|
form.setValue(
|
||||||
|
"temporaryAccess",
|
||||||
|
{
|
||||||
|
isTemporary: false
|
||||||
|
},
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Revoke Access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="mb-2 text-lg">Policies</div>
|
||||||
|
{(isCreate || !isLoading) && <PermissionEmptyState />}
|
||||||
|
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||||
|
<GeneralPermissionPolicies
|
||||||
|
subject={subject}
|
||||||
|
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||||
|
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||||
|
key={`project-permission-${subject}`}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
>
|
||||||
|
{renderConditionalComponents(subject, isDisabled)}
|
||||||
|
</GeneralPermissionPolicies>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</FormProvider>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { MemberProjectAdditionalPrivilegeSection } from "./MemberProjectAdditionalPrivilegeSection";
|
||||||
+23
-21
@@ -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 { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
IconButton,
|
|
||||||
TableContainer,
|
|
||||||
Td,
|
|
||||||
Table,
|
|
||||||
Tr,
|
|
||||||
Th,
|
|
||||||
THead,
|
|
||||||
TableSkeleton,
|
|
||||||
EmptyState,
|
EmptyState,
|
||||||
TBody,
|
IconButton,
|
||||||
Tooltip,
|
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Tag
|
Table,
|
||||||
} from "@app/components/v2";
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
Tag,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
@@ -28,10 +32,7 @@ import { usePopUp } from "@app/hooks";
|
|||||||
import { useUpdateUserWorkspaceRole } from "@app/hooks/api";
|
import { useUpdateUserWorkspaceRole } from "@app/hooks/api";
|
||||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||||
import { TWorkspaceUser } from "@app/hooks/api/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";
|
import { MemberRoleModify } from "./MemberRoleModify";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -44,6 +45,7 @@ export const MemberRoleDetailsSection = ({
|
|||||||
isMembershipDetailsLoading
|
isMembershipDetailsLoading
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
const userId = user?.id;
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
"deleteRole",
|
"deleteRole",
|
||||||
@@ -51,7 +53,7 @@ export const MemberRoleDetailsSection = ({
|
|||||||
] as const);
|
] as const);
|
||||||
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
|
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
|
||||||
|
|
||||||
const userId = user?.id;
|
const isOwnProjectMembershipDetails = userId === membershipDetails?.user?.id;
|
||||||
|
|
||||||
const handleRoleDelete = async () => {
|
const handleRoleDelete = async () => {
|
||||||
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
||||||
@@ -71,10 +73,10 @@ export const MemberRoleDetailsSection = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-4 w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
<h3 className="text-lg font-semibold text-mineshaft-100">Project Roles</h3>
|
<h3 className="text-lg font-semibold text-mineshaft-100">Project Roles</h3>
|
||||||
{userId !== membershipDetails?.user?.id && membershipDetails?.status !== "invited" && (
|
{!isOwnProjectMembershipDetails && membershipDetails?.status !== "invited" && (
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.Member}
|
a={ProjectPermissionSub.Member}
|
||||||
@@ -170,7 +172,7 @@ export const MemberRoleDetailsSection = ({
|
|||||||
ariaLabel="copy icon"
|
ariaLabel="copy icon"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
className="group relative"
|
className="group relative"
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handlePopUpOpen("deleteRole", {
|
handlePopUpOpen("deleteRole", {
|
||||||
@@ -191,7 +193,7 @@ export const MemberRoleDetailsSection = ({
|
|||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
{!isMembershipDetailsLoading && !membershipDetails?.roles?.length && (
|
{!isMembershipDetailsLoading && !membershipDetails?.roles?.length && (
|
||||||
<EmptyState title="This user has not been assigned to any projects" icon={faFolder} />
|
<EmptyState title="This user has no roles" icon={faFolder} />
|
||||||
)}
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+15
-6
@@ -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 { TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { MemberRbacSection } from "./MemberRbacSection";
|
import { MemberRbacSection } from "./MemberRbacSection";
|
||||||
import { SpecificPrivilegeSection } from "./SpecificPrivilegeSection";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectMember: TWorkspaceUser;
|
projectMember: TWorkspaceUser;
|
||||||
onOpenUpgradeModal: (title: string) => void;
|
onOpenUpgradeModal: (title: string) => void;
|
||||||
};
|
};
|
||||||
export const MemberRoleForm = ({ projectMember, onOpenUpgradeModal }: Props) => {
|
export const MemberRoleForm = ({ projectMember, onOpenUpgradeModal }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<MemberRbacSection
|
<MemberRbacSection projectMember={projectMember} onOpenUpgradeModal={onOpenUpgradeModal} />
|
||||||
projectMember={projectMember}
|
<Alert className="mt-4">
|
||||||
onOpenUpgradeModal={onOpenUpgradeModal}
|
<AlertDescription>
|
||||||
/>
|
Additional privileges now offer full permissions and have been moved to a new screen.
|
||||||
<SpecificPrivilegeSection membershipId={projectMember?.id} />
|
<br />
|
||||||
|
<Link href={`/project/${currentWorkspace?.id || ""}/members/${projectMember?.id}`}>
|
||||||
|
<span className="cursor-pointer text-primary">Click here to access them.</span>
|
||||||
|
</Link>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,5 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import {
|
import {
|
||||||
faClock,
|
faClock,
|
||||||
faEdit,
|
faEdit,
|
||||||
@@ -7,7 +8,6 @@ import {
|
|||||||
faUsers
|
faUsers
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const RolePage = withProjectPermission(
|
|||||||
if (!currentWorkspace?.slug || !data?.id) return;
|
if (!currentWorkspace?.slug || !data?.id) return;
|
||||||
|
|
||||||
await deleteProjectRole({
|
await deleteProjectRole({
|
||||||
projectSlug: currentWorkspace.slug,
|
projectId,
|
||||||
id: data.id
|
id: data.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { useWorkspace } from "@app/context";
|
|||||||
import {
|
import {
|
||||||
useCreateProjectRole,
|
useCreateProjectRole,
|
||||||
useGetProjectRoleBySlug,
|
useGetProjectRoleBySlug,
|
||||||
useUpdateProjectRole} from "@app/hooks/api";
|
useUpdateProjectRole
|
||||||
|
} from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
@@ -36,9 +37,9 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
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: createProjectRole } = useCreateProjectRole();
|
||||||
const { mutateAsync: updateProjectRole } = useUpdateProjectRole();
|
const { mutateAsync: updateProjectRole } = useUpdateProjectRole();
|
||||||
@@ -74,13 +75,13 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
|
|
||||||
const onFormSubmit = async ({ name, description, slug }: FormData) => {
|
const onFormSubmit = async ({ name, description, slug }: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!projectSlug) return;
|
if (!projectId) return;
|
||||||
|
|
||||||
if (role) {
|
if (role) {
|
||||||
// update
|
// update
|
||||||
await updateProjectRole({
|
await updateProjectRole({
|
||||||
id: role.id,
|
id: role.id,
|
||||||
projectSlug,
|
projectId,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
slug
|
slug
|
||||||
@@ -90,7 +91,7 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
} else {
|
} else {
|
||||||
// create
|
// create
|
||||||
const newRole = await createProjectRole({
|
const newRole = await createProjectRole({
|
||||||
projectSlug,
|
projectId,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
slug,
|
slug,
|
||||||
|
|||||||
+2
-2
@@ -71,7 +71,7 @@ const ConditionSchema = z
|
|||||||
{ message: "Duplicate operator found for a condition" }
|
{ message: "Duplicate operator found for a condition" }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const projectRoleFormSchema = z.object({
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
slug: z
|
slug: z
|
||||||
@@ -133,7 +133,7 @@ export const formSchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TFormSchema = z.infer<typeof formSchema>;
|
export type TFormSchema = z.infer<typeof projectRoleFormSchema>;
|
||||||
|
|
||||||
type TConditionalFields =
|
type TConditionalFields =
|
||||||
| ProjectPermissionSub.Secrets
|
| ProjectPermissionSub.Secrets
|
||||||
+9
-7
@@ -17,9 +17,9 @@ import { SecretPermissionConditions } from "./components/SecretPermissionConditi
|
|||||||
import { PermissionEmptyState } from "./PermissionEmptyState";
|
import { PermissionEmptyState } from "./PermissionEmptyState";
|
||||||
import {
|
import {
|
||||||
formRolePermission2API,
|
formRolePermission2API,
|
||||||
formSchema,
|
|
||||||
isConditionalSubjects,
|
isConditionalSubjects,
|
||||||
PROJECT_PERMISSION_OBJECT,
|
PROJECT_PERMISSION_OBJECT,
|
||||||
|
projectRoleFormSchema,
|
||||||
rolePermission2Form,
|
rolePermission2Form,
|
||||||
TFormSchema
|
TFormSchema
|
||||||
} from "./ProjectRoleModifySection.utils";
|
} from "./ProjectRoleModifySection.utils";
|
||||||
@@ -29,7 +29,10 @@ type Props = {
|
|||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderConditionalComponents = (subject: ProjectPermissionSub, isDisabled?: boolean) => {
|
export const renderConditionalComponents = (
|
||||||
|
subject: ProjectPermissionSub,
|
||||||
|
isDisabled?: boolean
|
||||||
|
) => {
|
||||||
if (subject === ProjectPermissionSub.Secrets)
|
if (subject === ProjectPermissionSub.Secrets)
|
||||||
return <SecretPermissionConditions isDisabled={isDisabled} />;
|
return <SecretPermissionConditions isDisabled={isDisabled} />;
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@ const renderConditionalComponents = (subject: ProjectPermissionSub, isDisabled?:
|
|||||||
export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
|
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
|
||||||
const projectSlug = currentWorkspace?.slug || "";
|
const projectId = currentWorkspace?.id || "";
|
||||||
const { data: role, isLoading } = useGetProjectRoleBySlug(
|
const { data: role, isLoading } = useGetProjectRoleBySlug(
|
||||||
currentWorkspace?.slug ?? "",
|
currentWorkspace?.slug ?? "",
|
||||||
roleSlug as string
|
roleSlug as string
|
||||||
@@ -51,7 +54,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
|||||||
|
|
||||||
const form = useForm<TFormSchema>({
|
const form = useForm<TFormSchema>({
|
||||||
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(projectRoleFormSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -64,14 +67,13 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async (el: TFormSchema) => {
|
const onSubmit = async (el: TFormSchema) => {
|
||||||
try {
|
try {
|
||||||
if (!projectSlug || !role?.id) return;
|
if (!projectId || !role?.id) return;
|
||||||
await updateRole({
|
await updateRole({
|
||||||
id: role?.id as string,
|
id: role?.id as string,
|
||||||
projectSlug,
|
projectId,
|
||||||
...el,
|
...el,
|
||||||
permissions: formRolePermission2API(el.permissions)
|
permissions: formRolePermission2API(el.permissions)
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({ type: "success", text: "Successfully updated role" });
|
createNotification({ type: "success", text: "Successfully updated role" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
|||||||
+4
-2
@@ -14,9 +14,9 @@ import {
|
|||||||
import { ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
formSchema,
|
|
||||||
isConditionalSubjects,
|
isConditionalSubjects,
|
||||||
PROJECT_PERMISSION_OBJECT,
|
PROJECT_PERMISSION_OBJECT,
|
||||||
|
projectRoleFormSchema,
|
||||||
TFormSchema
|
TFormSchema
|
||||||
} from "../ProjectRoleModifySection.utils";
|
} from "../ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
@@ -32,7 +32,9 @@ export const NewPermissionRule = ({ onClose }: Props) => {
|
|||||||
permissions: NonNullable<TFormSchema["permissions"]>;
|
permissions: NonNullable<TFormSchema["permissions"]>;
|
||||||
}>({
|
}>({
|
||||||
resolver: zodResolver(
|
resolver: zodResolver(
|
||||||
formSchema.pick({ permissions: true }).extend({ type: z.nativeEnum(ProjectPermissionSub) })
|
projectRoleFormSchema
|
||||||
|
.pick({ permissions: true })
|
||||||
|
.extend({ type: z.nativeEnum(ProjectPermissionSub) })
|
||||||
),
|
),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: ProjectPermissionSub.Secrets
|
type: ProjectPermissionSub.Secrets
|
||||||
|
|||||||
Reference in New Issue
Block a user