@@ -269,75 +220,61 @@ export const MemberListTab = () => {
- {isLoading && }
- {!isLoading &&
- filterdUsers?.map(
- ({ user: u, id: membershipId, roleId, role }) => {
- const name = u ? `${u.firstName} ${u.lastName}` : "-";
- const username = u?.username ?? "-";
- return (
-
- | {name} |
- {username} |
-
+ {isMembersLoading && }
+ {!isMembersLoading &&
+ filterdUsers?.map(({ user: u, inviteEmail, id: membershipId, roles }) => {
+ const name = u ? `${u.firstName} ${u.lastName}` : "-";
+ const email = u?.email || inviteEmail;
+
+ return (
+ |
+ | {name} |
+ {email} |
+
+
+ {(isAllowed) => (
+
+ handlePopUpOpen("upgradePlan", { description })
+ }
+ membershipId={membershipId}
+ />
+ )}
+
+ |
+
+ {userId !== u?.id && (
{(isAllowed) => (
-
+ handlePopUpOpen("removeMember", { email: u.email })}
+ >
+
+
)}
- |
-
- {userId !== u?.id && (
-
- {(isAllowed) => (
-
- handlePopUpOpen("removeMember", { username: u.username })
- }
- >
-
-
- )}
-
- )}
- |
-
- );
- }
- )}
+ )}
+
+
+ );
+ })}
- {!isLoading && filterdUsers?.length === 0 && (
+ {!isMembersLoading && filterdUsers?.length === 0 && (
)}
diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoles.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoles.tsx
new file mode 100644
index 000000000..107b97cc2
--- /dev/null
+++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoles.tsx
@@ -0,0 +1,473 @@
+import { useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { faCheck, faClock, faEdit, faSearch } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { twMerge } from "tailwind-merge";
+import { z } from "zod";
+
+import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
+import {
+ Button,
+ Checkbox,
+ FormControl,
+ HoverCard,
+ HoverCardContent,
+ HoverCardTrigger,
+ IconButton,
+ Input,
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+ Spinner,
+ Tag,
+ Tooltip
+} from "@app/components/v2";
+import { useSubscription, useWorkspace } from "@app/context";
+import { usePopUp } from "@app/hooks";
+import { useGetProjectRoles, useUpdateUserWorkspaceRole } from "@app/hooks/api";
+import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
+import { TWorkspaceUser } from "@app/hooks/api/types";
+import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
+import { groupBy } from "@app/lib/fn/array";
+
+const temporaryRoleFormSchema = z.object({
+ temporaryRange: z.string().min(1, "Required")
+});
+
+type TTemporaryRoleFormSchema = z.infer
;
+
+type TTemporaryRoleFormProps = {
+ temporaryConfig?: {
+ isTemporary?: boolean;
+ temporaryAccessEndTime?: string | null;
+ temporaryAccessStartTime?: string | null;
+ temporaryRange?: string | null;
+ };
+ onSetTemporary: (data: { temporaryRange: string; temporaryAccessStartTime?: string }) => void;
+ onRemoveTemporary: () => void;
+};
+
+const TemporaryRoleForm = ({
+ temporaryConfig: defaultValues = {},
+ onSetTemporary,
+ onRemoveTemporary
+}: TTemporaryRoleFormProps) => {
+ const { popUp, handlePopUpToggle } = usePopUp(["setTempRole"] as const);
+ const { control, handleSubmit } = useForm({
+ resolver: zodResolver(temporaryRoleFormSchema),
+ values: {
+ temporaryRange: defaultValues.temporaryRange || "1h"
+ }
+ });
+ const isTemporaryFieldValue = defaultValues.isTemporary;
+ const isExpired =
+ isTemporaryFieldValue && new Date() > new Date(defaultValues.temporaryAccessEndTime || "");
+
+ return (
+ {
+ handlePopUpToggle("setTempRole", isOpen);
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ Set Role Temporarily
+
+ {isExpired &&
Expired}
+
(
+
+ 1m, 2h, 3d.{" "}
+
+ More
+
+
+ }
+ >
+
+
+ )}
+ />
+
+ {isTemporaryFieldValue && (
+
+ )}
+ {!isTemporaryFieldValue ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
+
+const formSchema = z.record(
+ z.object({
+ isChecked: z.boolean().optional(),
+ temporaryAccess: z.union([
+ z.object({
+ isTemporary: z.literal(true),
+ temporaryRange: z.string().min(1),
+ temporaryAccessStartTime: z.string().datetime(),
+ temporaryAccessEndTime: z.string().datetime().nullable().optional()
+ }),
+ z.boolean()
+ ])
+ })
+);
+type TForm = z.infer;
+
+export type TMemberRolesProp = {
+ disableEdit?: boolean;
+ membershipId: string;
+ onOpenUpgradeModal: (description: string) => void;
+ roles: TWorkspaceUser["roles"];
+};
+
+const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
+
+export const MemberRoles = ({
+ roles = [],
+ disableEdit = false,
+ membershipId,
+ onOpenUpgradeModal
+}: TMemberRolesProp) => {
+ const { currentWorkspace } = useWorkspace();
+ const { createNotification } = useNotificationContext();
+ const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const);
+ const [searchRoles, setSearchRoles] = useState("");
+ const { subscription } = useSubscription();
+
+ const {
+ handleSubmit,
+ control,
+ reset,
+ setValue,
+ formState: { isSubmitting, isDirty }
+ } = useForm({
+ resolver: zodResolver(formSchema)
+ });
+
+ const workspaceId = currentWorkspace?.id || "";
+
+ const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
+ const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role);
+
+ const updateMembershipRole = useUpdateUserWorkspaceRole();
+
+ const handleRoleUpdate = async (data: TForm) => {
+ const selectedRoles = Object.keys(data)
+ .filter((el) => Boolean(data[el].isChecked))
+ .map((el) => {
+ const isTemporary = Boolean(data[el].temporaryAccess);
+ if (!isTemporary) {
+ return { role: el, isTemporary: false as const };
+ }
+
+ const tempCfg = data[el].temporaryAccess as {
+ temporaryRange: string;
+ temporaryAccessStartTime: string;
+ };
+
+ return {
+ role: el,
+ isTemporary: true as const,
+ temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
+ temporaryRange: tempCfg.temporaryRange,
+ temporaryAccessStartTime: tempCfg.temporaryAccessStartTime
+ };
+ });
+
+ const hasCustomRoleSelected = selectedRoles.some(
+ (el) => !Object.values(ProjectMembershipRole).includes(el.role as ProjectMembershipRole)
+ );
+
+ if (hasCustomRoleSelected && subscription && !subscription?.rbac) {
+ onOpenUpgradeModal(
+ "You can assign custom roles to members if you upgrade your Infisical plan."
+ );
+ return;
+ }
+
+ try {
+ await updateMembershipRole.mutateAsync({
+ workspaceId,
+ membershipId,
+ roles: selectedRoles
+ });
+ createNotification({ text: "Successfully updated role", type: "success" });
+ handlePopUpToggle("editRole");
+ setSearchRoles("");
+ } catch (err) {
+ createNotification({ text: "Failed to update role", type: "error" });
+ }
+ };
+
+ const formatRoleName = (role: string, customRoleName?: string) => {
+ if (role === ProjectMembershipRole.Custom) return customRoleName;
+ if (role === ProjectMembershipRole.Member) return "Developer";
+ return role;
+ };
+
+ return (
+
+ {roles
+ .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
+ .map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => {
+ const isExpired = new Date() > new Date(temporaryAccessEndTime || ("" as string));
+ return (
+
+
+
{formatRoleName(role, customRoleName)}
+ {isTemporary && (
+
+
+
+
+
+ )}
+
+
+ );
+ })}
+ {roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && (
+
+
+ +{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}
+
+
+ {roles
+ .slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
+ .map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => {
+ const isExpired = new Date() > new Date(temporaryAccessEndTime || ("" as string));
+ return (
+
+
+
{formatRoleName(role, customRoleName)}
+ {isTemporary && (
+
+
+ new Date(temporaryAccessEndTime as string) &&
+ "text-red-600"
+ )}
+ />
+
+
+ )}
+
+
+ );
+ })}{" "}
+
+
+ )}
+
+
{
+ handlePopUpToggle("editRole", isOpen);
+ reset();
+ }}
+ >
+ {!disableEdit && (
+
+
+
+
+
+ )}
+
+ {isRolesLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+};