mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: role list section completed with delete
This commit is contained in:
@@ -475,6 +475,7 @@ export const PROJECT_USERS = {
|
||||
},
|
||||
GET_USER_MEMBERSHIP: {
|
||||
workspaceId: "The ID of the project to get memberships from.",
|
||||
membershipId: "The ID of the user's project membership.",
|
||||
username: "The username to get project membership of. Email is the default username."
|
||||
},
|
||||
UPDATE_USER_MEMBERSHIP: {
|
||||
|
||||
@@ -56,7 +56,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
||||
})
|
||||
)
|
||||
})
|
||||
.omit({ createdAt: true, updatedAt: true })
|
||||
.omit({ updatedAt: true })
|
||||
.array()
|
||||
})
|
||||
}
|
||||
@@ -74,6 +74,64 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/memberships/:membershipId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Return project user membership",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
workspaceId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.workspaceId),
|
||||
membershipId: z.string().min(1).trim().describe(PROJECT_USERS.GET_USER_MEMBERSHIP.membershipId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
membership: ProjectMembershipsSchema.extend({
|
||||
user: UsersSchema.pick({
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true
|
||||
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
|
||||
roles: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
role: z.string(),
|
||||
customRoleId: z.string().optional().nullable(),
|
||||
customRoleName: z.string().optional().nullable(),
|
||||
customRoleSlug: z.string().optional().nullable(),
|
||||
isTemporary: z.boolean(),
|
||||
temporaryMode: z.string().optional().nullable(),
|
||||
temporaryRange: z.string().nullable().optional(),
|
||||
temporaryAccessStartTime: z.date().nullable().optional(),
|
||||
temporaryAccessEndTime: z.date().nullable().optional()
|
||||
})
|
||||
)
|
||||
}).omit({ updatedAt: true })
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const membership = await server.services.projectMembership.getProjectMembershipById({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
id: req.params.membershipId
|
||||
});
|
||||
return { membership };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:workspaceId/memberships/details",
|
||||
|
||||
@@ -158,6 +158,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("createdAt").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("isGhost").withSchema(TableName.Users),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
@@ -181,7 +182,18 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
|
||||
const members = sqlNestRelationships({
|
||||
data: docs,
|
||||
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId, projectName }) => ({
|
||||
parentMapper: ({
|
||||
email,
|
||||
firstName,
|
||||
username,
|
||||
lastName,
|
||||
publicKey,
|
||||
isGhost,
|
||||
id,
|
||||
userId,
|
||||
projectName,
|
||||
createdAt
|
||||
}) => ({
|
||||
isGroupMember: true,
|
||||
id,
|
||||
userId,
|
||||
@@ -190,7 +202,8 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
id: projectId,
|
||||
name: projectName
|
||||
},
|
||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost }
|
||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
|
||||
createdAt
|
||||
}),
|
||||
key: "id",
|
||||
childrenMapper: [
|
||||
|
||||
@@ -11,7 +11,10 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
const projectMemberOrm = ormify(db, TableName.ProjectMembership);
|
||||
|
||||
// special query
|
||||
const findAllProjectMembers = async (projectId: string, filter: { usernames?: string[]; username?: string } = {}) => {
|
||||
const findAllProjectMembers = async (
|
||||
projectId: string,
|
||||
filter: { usernames?: string[]; username?: string; id?: string } = {}
|
||||
) => {
|
||||
try {
|
||||
const docs = await db
|
||||
.replicaNode()(TableName.ProjectMembership)
|
||||
@@ -25,6 +28,9 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
if (filter.username) {
|
||||
void qb.where("username", filter.username);
|
||||
}
|
||||
if (filter.id) {
|
||||
void qb.where(`${TableName.ProjectMembership}.id`, filter.id);
|
||||
}
|
||||
})
|
||||
.join<TUserEncryptionKeys>(
|
||||
TableName.UserEncryptionKey,
|
||||
@@ -43,6 +49,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ProjectMembership),
|
||||
db.ref("createdAt").withSchema(TableName.ProjectMembership),
|
||||
db.ref("isGhost").withSchema(TableName.Users),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
@@ -66,7 +73,18 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
|
||||
const members = sqlNestRelationships({
|
||||
data: docs,
|
||||
parentMapper: ({ email, firstName, username, lastName, publicKey, isGhost, id, userId, projectName }) => ({
|
||||
parentMapper: ({
|
||||
email,
|
||||
firstName,
|
||||
username,
|
||||
lastName,
|
||||
publicKey,
|
||||
isGhost,
|
||||
id,
|
||||
userId,
|
||||
projectName,
|
||||
createdAt
|
||||
}) => ({
|
||||
id,
|
||||
userId,
|
||||
projectId,
|
||||
@@ -74,7 +92,8 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
project: {
|
||||
id: projectId,
|
||||
name: projectName
|
||||
}
|
||||
},
|
||||
createdAt
|
||||
}),
|
||||
key: "id",
|
||||
childrenMapper: [
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
TAddUsersToWorkspaceDTO,
|
||||
TDeleteProjectMembershipOldDTO,
|
||||
TDeleteProjectMembershipsDTO,
|
||||
TGetProjectMembershipByIdDTO,
|
||||
TGetProjectMembershipByUsernameDTO,
|
||||
TGetProjectMembershipDTO,
|
||||
TLeaveProjectDTO,
|
||||
@@ -133,6 +134,28 @@ export const projectMembershipServiceFactory = ({
|
||||
return membership;
|
||||
};
|
||||
|
||||
const getProjectMembershipById = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
id
|
||||
}: TGetProjectMembershipByIdDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { id });
|
||||
if (!membership) throw new NotFoundError({ message: `Project membership not found for user ${id}` });
|
||||
return membership;
|
||||
};
|
||||
|
||||
const addUsersToProject = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
@@ -487,6 +510,7 @@ export const projectMembershipServiceFactory = ({
|
||||
deleteProjectMemberships,
|
||||
deleteProjectMembership, // TODO: Remove this
|
||||
addUsersToProject,
|
||||
leaveProject
|
||||
leaveProject,
|
||||
getProjectMembershipById
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,6 +14,10 @@ export type TGetProjectMembershipByUsernameDTO = {
|
||||
username: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetProjectMembershipByIdDTO = {
|
||||
id: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateProjectMembershipDTO = {
|
||||
membershipId: string;
|
||||
roles: (
|
||||
|
||||
@@ -19,14 +19,14 @@ export const useCreateProjectRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TProjectRole, {}, TCreateProjectRoleDTO>({
|
||||
mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => {
|
||||
mutationFn: async ({ projectId, ...dto }: TCreateProjectRoleDTO) => {
|
||||
const {
|
||||
data: { role }
|
||||
} = await apiRequest.post(`/api/v2/workspace/${projectSlug}/roles`, dto);
|
||||
} = await apiRequest.post(`/api/v2/workspace/${projectId}/roles`, dto);
|
||||
return role;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -35,14 +35,14 @@ export const useUpdateProjectRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<TProjectRole, {}, TUpdateProjectRoleDTO>({
|
||||
mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => {
|
||||
mutationFn: async ({ id, projectId, ...dto }: TUpdateProjectRoleDTO) => {
|
||||
const {
|
||||
data: { role }
|
||||
} = await apiRequest.patch(`/api/v2/workspace/${projectSlug}/roles/${id}`, dto);
|
||||
} = await apiRequest.patch(`/api/v2/workspace/${projectId}/roles/${id}`, dto);
|
||||
return role;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -50,14 +50,14 @@ export const useUpdateProjectRole = () => {
|
||||
export const useDeleteProjectRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<TProjectRole, {}, TDeleteProjectRoleDTO>({
|
||||
mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => {
|
||||
mutationFn: async ({ projectId, id }: TDeleteProjectRoleDTO) => {
|
||||
const {
|
||||
data: { role }
|
||||
} = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/roles/${id}`);
|
||||
} = await apiRequest.delete(`/api/v2/workspace/${projectId}/roles/${id}`);
|
||||
return role;
|
||||
},
|
||||
onSuccess: (_, { projectSlug }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||
onSuccess: (_, { projectId }) => {
|
||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
||||
const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
||||
|
||||
export const roleQueryKeys = {
|
||||
getProjectRoles: (projectSlug: string) => ["roles", { projectSlug }] as const,
|
||||
getProjectRoles: (projectId: string) => ["roles", { projectSlug: projectId }] as const,
|
||||
getProjectRoleBySlug: (projectSlug: string, roleSlug: string) =>
|
||||
["roles", { projectSlug, roleSlug }] as const,
|
||||
getOrgRoles: (orgId: string) => ["org-roles", { orgId }] as const,
|
||||
@@ -56,11 +56,11 @@ export const getProjectRoles = async (projectId: string) => {
|
||||
return data.roles;
|
||||
};
|
||||
|
||||
export const useGetProjectRoles = (projectSlug: string) =>
|
||||
export const useGetProjectRoles = (projectId: string) =>
|
||||
useQuery({
|
||||
queryKey: roleQueryKeys.getProjectRoles(projectSlug),
|
||||
queryFn: () => getProjectRoles(projectSlug),
|
||||
enabled: Boolean(projectSlug)
|
||||
queryKey: roleQueryKeys.getProjectRoles(projectId),
|
||||
queryFn: () => getProjectRoles(projectId),
|
||||
enabled: Boolean(projectId)
|
||||
});
|
||||
|
||||
export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) =>
|
||||
|
||||
@@ -72,7 +72,7 @@ export type TDeleteOrgRoleDTO = {
|
||||
};
|
||||
|
||||
export type TCreateProjectRoleDTO = {
|
||||
projectSlug: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
slug: string;
|
||||
@@ -80,11 +80,11 @@ export type TCreateProjectRoleDTO = {
|
||||
};
|
||||
|
||||
export type TUpdateProjectRoleDTO = {
|
||||
projectSlug: string;
|
||||
projectId: string;
|
||||
id: string;
|
||||
} & Partial<Omit<TCreateProjectRoleDTO, "orgId">>;
|
||||
|
||||
export type TDeleteProjectRoleDTO = {
|
||||
projectSlug: string;
|
||||
projectId: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { UserWsKeyPair } from "../keys/types";
|
||||
import { ProjectUserMembershipTemporaryMode } from "../workspace/types";
|
||||
|
||||
export enum AuthMethod {
|
||||
EMAIL = "email",
|
||||
@@ -85,6 +86,7 @@ export type TWorkspaceUser = {
|
||||
id: string;
|
||||
publicKey: string;
|
||||
};
|
||||
createdAt: string;
|
||||
projectId: string;
|
||||
isGroupMember: boolean;
|
||||
project: {
|
||||
@@ -114,7 +116,7 @@ export type TWorkspaceUser = {
|
||||
customRoleSlug: string;
|
||||
isTemporary: true;
|
||||
temporaryRange: string;
|
||||
temporaryMode: string;
|
||||
temporaryMode: ProjectUserMembershipTemporaryMode;
|
||||
temporaryAccessEndTime: string;
|
||||
temporaryAccessStartTime: string;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export {
|
||||
useUpdateIdentityWorkspaceRole,
|
||||
useUpdateUserWorkspaceRole,
|
||||
useUpdateWsEnvironment,
|
||||
useUpgradeProject
|
||||
useUpgradeProject,
|
||||
useGetWorkspaceUserDetails
|
||||
} from "./queries";
|
||||
export { workspaceKeys } from "./query-keys";
|
||||
|
||||
@@ -365,6 +365,21 @@ export const useGetWorkspaceUsers = (workspaceId: string, includeGroupMembers?:
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetWorkspaceUserDetails = (workspaceId: string, membershipId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceUserDetails(workspaceId, membershipId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { membership }
|
||||
} = await apiRequest.get<{ membership: TWorkspaceUser }>(
|
||||
`/api/v1/workspace/${workspaceId}/memberships/${membershipId}`
|
||||
);
|
||||
return membership;
|
||||
},
|
||||
enabled: Boolean(workspaceId) && Boolean(membershipId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteUserFromWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ export const workspaceKeys = {
|
||||
getWorkspaceAuditLogs: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-audit-logs"] as const,
|
||||
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
|
||||
getWorkspaceUserDetails: (workspaceId: string, membershipId: string) =>
|
||||
[{ workspaceId, membershipId }, "workspace-user-details"] as const,
|
||||
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
||||
// allows invalidation using above key without knowing params
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/* 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() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<MemberDetailsPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Page.requireAuth = true;
|
||||
@@ -5,7 +5,7 @@ import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||
import { useOrganization,useWorkspace } from "@app/context";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useAddIdentityToWorkspace,
|
||||
useGetIdentityProjectMemberships,
|
||||
@@ -50,7 +50,7 @@ export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle
|
||||
const projectId = watch("projectId");
|
||||
const { data: projectMemberships } = useGetIdentityProjectMemberships(identityId);
|
||||
const { data: project } = useGetWorkspaceById(projectId);
|
||||
const { data: roles } = useGetProjectRoles(project?.slug ?? "");
|
||||
const { data: roles } = useGetProjectRoles(project?.id ?? "");
|
||||
|
||||
const filteredWorkspaces = useMemo(() => {
|
||||
const wsWorkspaceIds = new Map();
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Button } 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 { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
||||
|
||||
export const MemberDetailsPage = withProjectPermission(
|
||||
() => {
|
||||
const router = useRouter();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const membershipId = router.query.membershipId as string;
|
||||
|
||||
const { data: membershipDetails, isLoading: isMembershipDetailsLoading } =
|
||||
useGetWorkspaceUserDetails(workspaceId, membershipId);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 p-6 text-white">
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
variant="link"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||
onClick={() => {
|
||||
router.push(`/project/${workspaceId}/members`);
|
||||
}}
|
||||
className="mb-4"
|
||||
>
|
||||
Project Access Control
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-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 ">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Project User Management</h3>
|
||||
<div>
|
||||
<Button colorSchema="danger" variant="outline_bg" size="xs">
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
{membershipDetails && (
|
||||
<p className="capitalize">
|
||||
{membershipDetails.user.firstName || membershipDetails.user.lastName
|
||||
? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
|
||||
: "-"}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-2 text-sm text-gray-400">
|
||||
Joined on{" "}
|
||||
{membershipDetails?.createdAt &&
|
||||
format(new Date(membershipDetails?.createdAt || ""), "yyyy-MM-dd")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MemberRoleDetailsSection
|
||||
membershipDetails={membershipDetails}
|
||||
isMembershipDetailsLoading={isMembershipDetailsLoading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: ProjectPermissionActions.Read,
|
||||
subject: ProjectPermissionSub.Member
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,144 @@
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
IconButton,
|
||||
TableContainer,
|
||||
Td,
|
||||
Table,
|
||||
Tr,
|
||||
Th,
|
||||
THead,
|
||||
TableSkeleton,
|
||||
EmptyState,
|
||||
TBody,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useUser, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useDeleteProjectRole,
|
||||
useGetWorkspaceUserDetails,
|
||||
useUpdateUserWorkspaceRole
|
||||
} from "@app/hooks/api";
|
||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||
import { faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
type Props = {
|
||||
membershipDetails: TWorkspaceUser;
|
||||
isMembershipDetailsLoading?: boolean;
|
||||
};
|
||||
|
||||
export const MemberRoleDetailsSection = ({
|
||||
membershipDetails,
|
||||
isMembershipDetailsLoading
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
const { user } = useUser();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||
"deleteRole"
|
||||
] as const);
|
||||
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
|
||||
|
||||
const userId = user?.id;
|
||||
|
||||
const handleRoleDelete = async () => {
|
||||
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
||||
try {
|
||||
const updatedRole = membershipDetails?.roles?.filter((el) => el.id !== id);
|
||||
await updateUserWorkspaceRole({
|
||||
workspaceId: currentWorkspace?.id || "",
|
||||
roles: updatedRole,
|
||||
membershipId: membershipDetails.id
|
||||
});
|
||||
createNotification({ type: "success", text: "Successfully removed the role" });
|
||||
handlePopUpClose("deleteRole");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({ type: "error", text: "Failed to delete role" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="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 Roles</h3>
|
||||
{userId !== membershipDetails?.user?.id && membershipDetails?.status !== "invited" && (
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Role</Th>
|
||||
<Th>Type</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isMembershipDetailsLoading && (
|
||||
<TableSkeleton columns={3} innerKey="user-project-memberships" />
|
||||
)}
|
||||
{!isMembershipDetailsLoading &&
|
||||
membershipDetails?.roles?.map((roleDetails) => {
|
||||
return (
|
||||
<Tr className="group h-10" key={`user-project-membership-${roleDetails?.id}`}>
|
||||
<Td>
|
||||
{roleDetails.role === "custom"
|
||||
? roleDetails.customRoleName
|
||||
: roleDetails.role}
|
||||
</Td>
|
||||
<Td>{!roleDetails.isTemporary ? "permanent" : "temporary"}</Td>
|
||||
<Td>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<Tooltip content="Remove">
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", {
|
||||
id: roleDetails?.id,
|
||||
slug: roleDetails?.customRoleSlug || roleDetails?.role
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isMembershipDetailsLoading && !membershipDetails?.roles?.length && (
|
||||
<EmptyState title="This user has not been assigned to any projects" icon={faFolder} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteRole.isOpen}
|
||||
deleteKey="remove"
|
||||
title={`Do you want to remove role ${(popUp?.deleteRole?.data as TProjectRole)?.slug}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteRole", isOpen)}
|
||||
onDeleteApproved={() => handleRoleDelete()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { MemberRoleDetailsSection } from "./MemberRoleDetailsSection";
|
||||
1
frontend/src/views/Project/MemberDetailsPage/index.tsx
Normal file
1
frontend/src/views/Project/MemberDetailsPage/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { MemberDetailsPage } from "./MemberDetailPage";
|
||||
@@ -32,12 +32,11 @@ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const orgId = currentOrg?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
|
||||
const { data: groups } = useGetOrganizationGroups(orgId);
|
||||
const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.id || "");
|
||||
|
||||
const { data: roles } = useGetProjectRoles(projectSlug);
|
||||
const { data: roles } = useGetProjectRoles(currentWorkspace?.id || "");
|
||||
|
||||
const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace();
|
||||
|
||||
|
||||
@@ -216,9 +216,9 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
|
||||
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
|
||||
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(
|
||||
currentWorkspace?.id ?? ""
|
||||
);
|
||||
const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role);
|
||||
|
||||
const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole();
|
||||
|
||||
@@ -42,7 +42,6 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
|
||||
const organizationId = currentOrg?.id || "";
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
|
||||
const { data: identityMembershipOrgsData } = useGetIdentityMembershipOrgs({
|
||||
organizationId,
|
||||
@@ -59,7 +58,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
data: roles,
|
||||
isLoading: isRolesLoading,
|
||||
isFetched: isRolesFetched
|
||||
} = useGetProjectRoles(projectSlug);
|
||||
} = useGetProjectRoles(workspaceId);
|
||||
|
||||
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
|
||||
|
||||
|
||||
@@ -65,8 +65,7 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
|
||||
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||
const { permission } = useProjectPermission();
|
||||
const isMemberEditDisabled = permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
|
||||
@@ -54,7 +54,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
||||
const { data: orgUsers } = useGetOrgUsers(orgId);
|
||||
|
||||
const { data: roles } = useGetProjectRoles(currentWorkspace?.slug || "");
|
||||
const { data: roles } = useGetProjectRoles(currentWorkspace?.id || "");
|
||||
|
||||
const {
|
||||
control,
|
||||
|
||||
@@ -65,8 +65,7 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
|
||||
const { subscription } = useSubscription();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
|
||||
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||
const { permission } = useProjectPermission();
|
||||
const isMemberEditDisabled = permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
|
||||
@@ -7,6 +7,7 @@ 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";
|
||||
@@ -65,6 +66,7 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { user } = useUser();
|
||||
const router = useRouter();
|
||||
|
||||
const userId = user?.id || "";
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
@@ -111,7 +113,18 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop
|
||||
const email = u?.email || inviteEmail;
|
||||
|
||||
return (
|
||||
<Tr key={`membership-${membershipId}`} className="group w-full">
|
||||
<Tr
|
||||
key={`membership-${membershipId}`}
|
||||
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
router.push(`/project/${workspaceId}/members/${membershipId}`);
|
||||
}
|
||||
}}
|
||||
onClick={() => router.push(`/project/${workspaceId}/members/${membershipId}`)}
|
||||
>
|
||||
<Td>{name}</Td>
|
||||
<Td>{email}</Td>
|
||||
<Td>
|
||||
@@ -202,9 +215,11 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop
|
||||
size="sm"
|
||||
variant="plain"
|
||||
ariaLabel="update-role"
|
||||
onClick={() =>
|
||||
handlePopUpOpen("updateRole", { ...projectMember, index })
|
||||
}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("updateRole", { ...projectMember, index });
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
@@ -226,9 +241,11 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={userId === u?.id || !isAllowed}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("removeMember", { username: u.username })
|
||||
}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
handlePopUpOpen("removeMember", { username: u.username });
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
|
||||
@@ -34,10 +34,9 @@ export const ProjectRoleList = () => {
|
||||
"deleteRole"
|
||||
] as const);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
const projectId = currentWorkspace?.id || "";
|
||||
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(projectId);
|
||||
|
||||
const { mutateAsync: deleteRole } = useDeleteProjectRole();
|
||||
|
||||
@@ -45,7 +44,7 @@ export const ProjectRoleList = () => {
|
||||
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
||||
try {
|
||||
await deleteRole({
|
||||
projectSlug,
|
||||
projectId,
|
||||
id
|
||||
});
|
||||
createNotification({ type: "success", text: "Successfully removed the role" });
|
||||
|
||||
Reference in New Issue
Block a user