Finish preliminary making user settings, org settings styling similar to usage and billing page

This commit is contained in:
Tuan Dang
2023-06-29 17:47:23 +07:00
parent 81c69d92b3
commit 056f5a4555
73 changed files with 2098 additions and 1698 deletions
@@ -0,0 +1,120 @@
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useAddIncidentContact
} from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { UsePopUpState } from "@app/hooks/usePopUp";
const addContactFormSchema = yup.object({
email: yup.string().email().required().label("Email").trim()
});
type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
type Props = {
popUp: UsePopUpState<["addContact"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addContact"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addContact"]>, state?: boolean) => void;
};
export const AddOrgIncidentContactModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { data: serverDetails } = useFetchServerStatus()
const {
control,
handleSubmit,
reset
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
const { mutateAsync, isLoading } = useAddIncidentContact();
const onFormSubmit = async ({ email }: TAddContactForm) => {
try {
if (!currentOrg?._id) return;
await mutateAsync({
orgId: currentOrg._id,
email
});
createNotification({
text: "Successfully added incident contact",
type: "success"
});
if (serverDetails?.emailConfigured){
handlePopUpClose("addContact");
}
reset();
} catch (err) {
console.error(err);
createNotification({
text: "Failed to add incident contact",
type: "error"
});
}
}
return (
<Modal
isOpen={popUp?.addContact?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addContact", isOpen);
reset();
}}
>
<ModalContent
title="Add an Incident Contact"
subTitle="This contact will be notified in the unlikely event of a severe incident."
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="email"
render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
size="sm"
type="submit"
isLoading={isLoading}
isDisabled={isLoading}
>
Add Incident Contact
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addContact")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,43 @@
import { useTranslation } from "react-i18next";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button
} from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal";
import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
export const OrgIncidentContactsSection = () => {
const { t } = useTranslation();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addContact"
] as const);
return (
<div className="mb-6 p-4 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-8">
<p className="min-w-max text-xl font-semibold">
{t("section.incident.incident-contacts")}
</p>
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addContact")}
>
Add contact
</Button>
</div>
<OrgIncidentContactsTable />
<AddOrgIncidentContactModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
</div>
);
}
@@ -0,0 +1,117 @@
import { useState } from "react";
import {
faContactBook,
faMagnifyingGlass,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
DeleteActionModal,
EmptyState,
IconButton,
Input,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useDeleteIncidentContact,
useGetOrgIncidentContact} from "@app/hooks/api";
export const OrgIncidentContactsTable = () => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { data: contacts, isLoading } = useGetOrgIncidentContact(currentOrg?._id ?? "");
const [searchContact, setSearchContact] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeContact",
"setUpEmail"
] as const);
const { mutateAsync } = useDeleteIncidentContact();
const onRemoveIncidentContact = async () => {
try {
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
if (!currentOrg?._id) return;
await mutateAsync({
orgId: currentOrg._id,
email: incidentContactEmail
});
createNotification({
text: "Successfully removed incident contact",
type: "success"
});
handlePopUpClose("removeContact");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to remove incident contact",
type: "error"
});
}
};
const filteredContacts = contacts ? contacts.filter(({ email }) =>
email.toLocaleLowerCase().includes(searchContact)
) : [];
return (
<div>
<Input
value={searchContact}
onChange={(e) => setSearchContact(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search incident contact by email..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Email</Th>
<Th aria-label="actions" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
{filteredContacts?.map(({ email }) => (
<Tr key={email}>
<Td className="w-full">{email}</Td>
<Td className="mr-4">
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen("removeContact", { email })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Td>
</Tr>
))}
</TBody>
</Table>
{filteredContacts?.length === 0 && !isLoading && (
<EmptyState title="No incident contacts found" icon={faContactBook} />
)}
</TableContainer>
<DeleteActionModal
isOpen={popUp.removeContact.isOpen}
deleteKey="remove"
title="Do you want to remove this email from incident contact?"
onChange={(isOpen) => handlePopUpToggle("removeContact", isOpen)}
onDeleteApproved={onRemoveIncidentContact}
/>
</div>
);
};
@@ -0,0 +1,2 @@
export { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal"
export { OrgIncidentContactsSection } from "./OrgIncidentContactsSection";
@@ -1,200 +0,0 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import {
faContactBook,
faMagnifyingGlass,
faPlus,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import {
Button,
DeleteActionModal,
EmailServiceSetupModal,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { IncidentContact } from "@app/hooks/api/types";
type Props = {
isLoading?: boolean;
contacts?: IncidentContact[];
onRemoveContact: (email: string) => Promise<void>;
onAddContact: (email: string) => Promise<void>;
};
const addContactFormSchema = yup.object({
email: yup.string().email().required().label("Email").trim()
});
type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
export const OrgIncidentContactsTable = ({
contacts = [],
onAddContact,
onRemoveContact,
isLoading
}: Props) => {
const [searchContact, setSearchContact] = useState("");
const {data: serverDetails } = useFetchServerStatus()
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addContact",
"removeContact",
"setUpEmail"
] as const);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
const onAddIncidentContact = ({ email }: TAddContactForm) => {
onAddContact(email);
handlePopUpClose("addContact");
reset();
};
const onRemoveIncidentContact = async () => {
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
await onRemoveContact(incidentContactEmail);
handlePopUpClose("removeContact");
};
const filteredContacts = contacts.filter(({ email }) =>
email.toLocaleLowerCase().includes(searchContact)
);
return (
<div className="w-full">
<div className="mb-4 flex">
<div className="mr-4 flex-1">
<Input
value={searchContact}
onChange={(e) => setSearchContact(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search incident contact by email..."
/>
</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (serverDetails?.emailConfigured){
handlePopUpOpen("addContact");
} else {
handlePopUpOpen("setUpEmail");
}
}}
>
Add Contact
</Button>
</div>
</div>
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Email</Th>
<Th aria-label="actions" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
{filteredContacts?.map(({ email }) => (
<Tr key={email}>
<Td className="w-full">{email}</Td>
<Td className="mr-4">
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen("removeContact", { email })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Td>
</Tr>
))}
</TBody>
</Table>
{filteredContacts?.length === 0 && !isLoading && (
<EmptyState title="No incident contacts found" icon={faContactBook} />
)}
</TableContainer>
</div>
<Modal
isOpen={popUp?.addContact?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addContact", isOpen);
reset();
}}
>
<ModalContent
title="Add an Incident Contact"
subTitle="This contact will be notified in the unlikely event of a severe incident."
>
<form onSubmit={handleSubmit(onAddIncidentContact)}>
<Controller
control={control}
defaultValue=""
name="email"
render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add Incident Contact
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addContact")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeContact.isOpen}
deleteKey="remove"
title="Do you want to remove this email from incident contact?"
onChange={(isOpen) => handlePopUpToggle("removeContact", isOpen)}
onDeleteApproved={onRemoveIncidentContact}
/>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
/>
</div>
);
};
@@ -1 +0,0 @@
export { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
@@ -0,0 +1,174 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
IconButton,
Input,
Modal,
ModalContent
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useToggle } from "@app/hooks";
import {
useAddUserToOrg
} from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { UsePopUpState } from "@app/hooks/usePopUp";
const addMemberFormSchema = yup.object({
email: yup.string().email().required().label("Email").trim()
});
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>; // TODO: change to FormData
type Props = {
popUp: UsePopUpState<["addMember"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addMember"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void;
};
// TODO: test no-SMTP setup case
export const AddOrgMemberModal = ({
popUp,
handlePopUpToggle,
handlePopUpClose
}: Props) => {
const { currentOrg } = useOrganization();
const { data: serverDetails } = useFetchServerStatus();
const { createNotification } = useNotificationContext();
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const [completeInviteLink, setCompleteInviteLink] = useState<string | undefined>("");
const {
control,
handleSubmit,
reset
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
const { mutateAsync, isLoading } = useAddUserToOrg();
useEffect(() => {
let timer: NodeJS.Timeout;
if (isInviteLinkCopied) {
timer = setTimeout(() => setInviteLinkCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isInviteLinkCopied]);
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(completeInviteLink as string);
setInviteLinkCopied.on();
};
const onFormSubmit = async ({ email }: TAddMemberForm) => {
try {
if (!currentOrg?._id) return;
const { data } = await mutateAsync({
organizationId: currentOrg?._id,
inviteeEmail: email
});
setCompleteInviteLink(data?.completeInviteLink);
if (!data.completeInviteLink) {
createNotification({
text: "Successfully sent an invite to the user.",
type: "success"
});
}
if (serverDetails?.emailConfigured){
handlePopUpClose("addMember");
}
reset();
} catch (err) {
console.error(err);
createNotification({
text: "Failed to send an invite to the user",
type: "error"
});
}
}
return (
<Modal
isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addMember", isOpen);
setCompleteInviteLink(undefined);
}}
>
<ModalContent
title={`Invite others to ${currentOrg?.name ?? ""}`}
subTitle={
<div>
{!completeInviteLink && <div>
An invite is specific to an email address and expires after 1 day.
<br />
For security reasons, you will need to separately add members to projects.
</div>}
{completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
</div>
}
>
{!completeInviteLink && (
<form onSubmit={handleSubmit(onFormSubmit)} >
<Controller
control={control}
defaultValue=""
name="email"
render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isLoading}
isDisabled={isLoading}
>
Add Member
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addMember")}
>
Cancel
</Button>
</div>
</form>
)}
{completeInviteLink && (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{completeInviteLink}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span>
</IconButton>
</div>
)}
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,57 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
UpgradePlanModal
} from "@app/components/v2";
import { useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
import { AddOrgMemberModal } from "./AddOrgMemberModal";
import { OrgMembersTable } from "./OrgMembersTable";
export const OrgMembersSection = () => {
const { subscription } = useSubscription();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addMember",
"upgradePlan",
] as const);
const isMoreMembersAllowed = subscription?.memberLimit ? (subscription.membersUsed < subscription.memberLimit) : true;
return (
<div className="mb-6 p-4 bg-mineshaft-900 max-w-screen-lg rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">
Organization members
</p>
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isMoreMembersAllowed) {
handlePopUpOpen("addMember");
return;
}
handlePopUpOpen("upgradePlan");
}}
>
Add Member
</Button>
</div>
<OrgMembersTable />
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="Add more members by upgrading to a higher Infisical plan."
/>
<AddOrgMemberModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
handlePopUpClose={handlePopUpClose}
/>
</div>
);
}
@@ -1,20 +1,19 @@
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useMemo, useState } from "react";
import { useRouter } from "next/router";
import { faCheck, faCopy, faMagnifyingGlass, faPlus, faTrash, faUsers } from "@fortawesome/free-solid-svg-icons";
import { faMagnifyingGlass, faPlus, faTrash, faUsers } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import {
Button,
DeleteActionModal,
EmailServiceSetupModal, EmptyState,
FormControl,
EmptyState,
IconButton,
Input,
Modal,
ModalContent,
Select,
SelectItem,
Table,
@@ -25,73 +24,63 @@ import {
Td,
Th,
THead,
Tr,
UpgradePlanModal} from "@app/components/v2";
import { usePopUp, useToggle } from "@app/hooks";
Tr
} from "@app/components/v2";
import { useOrganization, useUser, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useAddUserToOrg,
useDeleteOrgMembership,
useGetOrgUsers,
useGetUserWorkspaceMemberships,
useGetUserWsKey,
useUpdateOrgUserRole,
useUploadWsKey
} from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { OrgUser, Workspace } from "@app/hooks/api/types";
type Props = {
members?: OrgUser[];
workspaceMemberships?: Record<string, Workspace[]>;
orgName: string;
isLoading?: boolean;
isMoreUserNotAllowed: boolean;
onRemoveMember: (userId: string) => Promise<void>;
onInviteMember: (email: string) => Promise<void>;
onRoleChange: (membershipId: string, role: string) => Promise<void>;
onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
// the current user id to block remove org button
userId: string;
completeInviteLink: string | undefined,
setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>>
};
const addMemberFormSchema = yup.object({
email: yup.string().email().required().label("Email").trim()
});
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
export const OrgMembersTable = ({
members = [],
workspaceMemberships = {},
orgName,
isMoreUserNotAllowed,
onRemoveMember,
onInviteMember,
onGrantAccess,
onRoleChange,
userId,
isLoading,
completeInviteLink,
setCompleteInviteLink
}: Props) => {
export const OrgMembersTable = () => {
const router = useRouter();
const { user: currentUser } = useUser();
const { currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const {data: serverDetails } = useFetchServerStatus()
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const { data: serverDetails } = useFetchServerStatus()
const { data: members, isLoading: isOrgUserLoading } = useGetOrgUsers(currentOrg?._id ?? ""); // members
const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } = useGetUserWorkspaceMemberships(currentOrg?._id ?? "");
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || "");
const uploadWsKey = useUploadWsKey();
const addUserToOrg = useAddUserToOrg();
const removeUserOrgMembership = useDeleteOrgMembership();
const updateOrgUserRole = useUpdateOrgUserRole();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addMember",
"removeMember",
"upgradePlan",
"setUpEmail"
] as const);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
const isLoading = isOrgUserLoading || IsWsMembershipLoading;
const userId = currentUser?._id || "";
const onRemoveMember = async (membershipId: string) => {
if (!currentOrg?._id) return;
const onAddMember = async ({ email }: TAddMemberForm) => {
await onInviteMember(email);
if (serverDetails?.emailConfigured){
handlePopUpClose("addMember");
}
reset();
try {
if (!currentOrg?._id) return;
await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId });
createNotification({
text: "Successfully removed user from org",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to remove user from the organization",
type: "error"
});
}
};
const onRemoveOrgMemberApproved = async () => {
@@ -100,62 +89,111 @@ export const OrgMembersTable = ({
handlePopUpClose("removeMember");
};
const onInviteMember = async (email: string) => {
if (!currentOrg?._id) return;
try {
const { data } = await addUserToOrg.mutateAsync({
organizationId: currentOrg?._id,
inviteeEmail: email
});
// only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured
if (!data.completeInviteLink) {
createNotification({
text: "Successfully invited user to the organization.",
type: "success"
});
}
} catch (error) {
console.error(error);
createNotification({
text: "Failed to invite user to org",
type: "error"
});
}
};
const onGrantAccess = async (targetUserId: string, publicKey: string) => {
try {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
if (!PRIVATE_KEY || !wsKey) return;
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey,
privateKey: PRIVATE_KEY
});
await uploadWsKey.mutateAsync({
userId: targetUserId,
nonce,
encryptedKey: ciphertext,
workspaceId: currentWorkspace?._id || ""
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to grant access to user",
type: "error"
});
}
};
const onRoleChange = async (membershipId: string, role: string) => {
if (!currentOrg?._id) return;
try {
await updateOrgUserRole.mutateAsync({ organizationId: currentOrg?._id, membershipId, role });
createNotification({
text: "Successfully updated user role",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to update user role",
type: "error"
});
}
};
const isIamOwner = useMemo(
() => members.find(({ user }) => userId === user?._id)?.role === "owner",
() => members ? members.find(({ user }) => userId === user?._id)?.role === "owner" : [],
[userId, members]
);
const filterdUser = useMemo(
() =>
members.filter(
members ? members.filter(
({ user, inviteEmail }) =>
user?.firstName?.toLowerCase().includes(searchMemberFilter) ||
user?.lastName?.toLowerCase().includes(searchMemberFilter) ||
user?.email?.toLowerCase().includes(searchMemberFilter) ||
inviteEmail?.includes(searchMemberFilter)
),
) : [],
[members, searchMemberFilter]
);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isInviteLinkCopied) {
timer = setTimeout(() => setInviteLinkCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isInviteLinkCopied]);
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(completeInviteLink as string);
setInviteLinkCopied.on();
};
return (
<div className="w-full">
<div className="mb-4 flex">
<div className="mr-4 flex-1">
<Input
<Input
value={searchMemberFilter}
onChange={(e) => setSearchMemberFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
</div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isMoreUserNotAllowed) {
handlePopUpOpen("upgradePlan");
} else {
handlePopUpOpen("addMember");
}
}}
>
Add Member
</Button>
</div>
<div>
<TableContainer>
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
@@ -167,7 +205,7 @@ export const OrgMembersTable = ({
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={5} key="org-members" />}
{isLoading && <TableSkeleton columns={5} />}
{!isLoading &&
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
const name = user ? `${user.firstName} ${user.lastName}` : "-";
@@ -213,7 +251,7 @@ export const OrgMembersTable = ({
<Td>
{userWs ? (
userWs?.map(({ name: wsName, _id }) => (
<Tag key={`user-${user._id}-workspace-${_id}`} className="my-1">
<Tag key={`user-${currentUser._id}-workspace-${_id}`} className="my-1">
{wsName}
</Tag>
))
@@ -254,74 +292,6 @@ export const OrgMembersTable = ({
<EmptyState title="No project members found" icon={faUsers} />
)}
</TableContainer>
</div>
<Modal
isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addMember", isOpen);
setCompleteInviteLink(undefined)
}}
>
<ModalContent
title={`Invite others to ${orgName}`}
subTitle={
<div>
{!completeInviteLink && <div>
An invite is specific to an email address and expires after 1 day.
<br />
For security reasons, you will need to separately add members to projects.
</div>}
{completeInviteLink && "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
</div>
}
>
{!completeInviteLink && <form onSubmit={handleSubmit(onAddMember)} >
<Controller
control={control}
defaultValue=""
name="email"
render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add Member
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addMember")}
>
Cancel
</Button>
</div>
</form>}
{
completeInviteLink &&
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{completeInviteLink}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">click to copy</span>
</IconButton>
</div>
}
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
deleteKey="remove"
@@ -329,15 +299,6 @@ export const OrgMembersTable = ({
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
onDeleteApproved={onRemoveOrgMemberApproved}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan."
/>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
/>
</div>
);
};
@@ -0,0 +1 @@
export { OrgMembersSection } from "./OrgMembersSection";
@@ -1 +0,0 @@
export { OrgMembersTable } from "./OrgMembersTable";
@@ -1,6 +1,5 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
@@ -16,7 +15,6 @@ const formSchema = yup.object({
type FormData = yup.InferType<typeof formSchema>;
export const OrgNameChangeSection = (): JSX.Element => {
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const {
@@ -54,9 +52,11 @@ export const OrgNameChangeSection = (): JSX.Element => {
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600 "
className="p-4 bg-mineshaft-900 mb-6 max-w-screen-lg rounded-lg border border-mineshaft-600"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-8">{t("common.display-name")}</p>
<p className="text-xl font-semibold text-mineshaft-100 mb-8">
Organization name
</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""
@@ -1,5 +1,8 @@
import { useEffect, useMemo,useState } from "react";
import { Controller,useForm } from "react-hook-form";
import {
// Controller,
// useForm
} from "react-hook-form";
import { useRouter } from "next/router";
import {
faCheck,
@@ -10,21 +13,21 @@ import {
faServer,
faTrash} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { generateKeyPair } from "@app/components/utilities/cryptography/crypto";
// import { yupResolver } from "@hookform/resolvers/yup";
// import * as yup from "yup";
// import { generateKeyPair } from "@app/components/utilities/cryptography/crypto";
import {
Button,
DeleteActionModal,
EmptyState,
FormControl,
// FormControl,
IconButton,
Input,
Modal,
ModalContent,
Select,
SelectItem,
// Select,
// SelectItem,
Table,
TableContainer,
TableSkeleton,
@@ -37,25 +40,26 @@ import {
import { useOrganization, useWorkspace } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
useCreateServiceAccount,
// useCreateServiceAccount,
useDeleteServiceAccount,
useGetServiceAccounts} from "@app/hooks/api";
useGetServiceAccounts
} from "@app/hooks/api";
const serviceAccountExpiration = [
{ label: "1 Day", value: 86400 },
{ label: "7 Days", value: 604800 },
{ label: "1 Month", value: 2592000 },
{ label: "6 months", value: 15552000 },
{ label: "12 months", value: 31104000 },
{ label: "Never", value: -1 }
];
// const serviceAccountExpiration = [
// { label: "1 Day", value: 86400 },
// { label: "7 Days", value: 604800 },
// { label: "1 Month", value: 2592000 },
// { label: "6 months", value: 15552000 },
// { label: "12 months", value: 31104000 },
// { label: "Never", value: -1 }
// ];
const addServiceAccountFormSchema = yup.object({
name: yup.string().required().label("Name").trim(),
expiresIn: yup.string().required().label("Service Account Expiration")
});
// const addServiceAccountFormSchema = yup.object({
// name: yup.string().required().label("Name").trim(),
// expiresIn: yup.string().required().label("Service Account Expiration")
// });
type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
export const OrgServiceAccountsTable = () => {
const router = useRouter();
@@ -67,9 +71,9 @@ export const OrgServiceAccountsTable = () => {
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
const [accessKey, setAccessKey] = useState("");
const [publicKey, setPublicKey] = useState("");
const [privateKey, setPrivateKey] = useState("");
const [accessKey] = useState("");
const [publicKey] = useState("");
const [privateKey] = useState("");
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addServiceAccount",
@@ -78,7 +82,7 @@ export const OrgServiceAccountsTable = () => {
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId);
const createServiceAccount = useCreateServiceAccount();
// const createServiceAccount = useCreateServiceAccount();
const removeServiceAccount = useDeleteServiceAccount();
useEffect(() => {
@@ -98,32 +102,32 @@ export const OrgServiceAccountsTable = () => {
return () => clearTimeout(timer);
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
// const {
// control,
// handleSubmit,
// reset,
// formState: { isSubmitting }
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
if (!currentOrg?._id) return;
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
// if (!currentOrg?._id) return;
const keyPair = generateKeyPair();
setPublicKey(keyPair.publicKey);
setPrivateKey(keyPair.privateKey);
// const keyPair = generateKeyPair();
// setPublicKey(keyPair.publicKey);
// setPrivateKey(keyPair.privateKey);
const serviceAccountDetails = await createServiceAccount.mutateAsync({
name,
organizationId: currentOrg?._id,
publicKey: keyPair.publicKey,
expiresIn: Number(expiresIn)
});
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
// name,
// organizationId: currentOrg?._id,
// publicKey: keyPair.publicKey,
// expiresIn: Number(expiresIn)
// });
setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
setStep(1);
reset();
}
// setStep(1);
// reset();
// }
const onRemoveServiceAccount = async () => {
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
@@ -144,63 +148,67 @@ export const OrgServiceAccountsTable = () => {
switch (stepToRender) {
case 0:
return (
<form onSubmit={handleSubmit(onAddServiceAccount)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="expiresIn"
defaultValue={String(serviceAccountExpiration?.[0]?.value)}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Expiration"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{serviceAccountExpiration.map(({ label, value }) => (
<SelectItem value={String(value)} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
);
}}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create Service Account
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addServiceAccount")}
>
Cancel
</Button>
</div>
</form>
<div>
We are currently revising the service account mechanism. In the meantime,
please use service tokens or API key to fetch secrets via API request.
</div>
// <form onSubmit={handleSubmit(onAddServiceAccount)}>
// <Controller
// control={control}
// defaultValue=""
// name="name"
// render={({ field, fieldState: { error } }) => (
// <FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
// <Input {...field} />
// </FormControl>
// )}
// />
// <Controller
// control={control}
// name="expiresIn"
// defaultValue={String(serviceAccountExpiration?.[0]?.value)}
// render={({ field: { onChange, ...field }, fieldState: { error } }) => {
// return (
// <FormControl
// label="Expiration"
// errorText={error?.message}
// isError={Boolean(error)}
// >
// <Select
// defaultValue={field.value}
// {...field}
// onValueChange={(e) => onChange(e)}
// className="w-full"
// >
// {serviceAccountExpiration.map(({ label, value }) => (
// <SelectItem value={String(value)} key={label}>
// {label}
// </SelectItem>
// ))}
// </Select>
// </FormControl>
// );
// }}
// />
// <div className="mt-8 flex items-center">
// <Button
// className="mr-4"
// size="sm"
// type="submit"
// isLoading={isSubmitting}
// isDisabled={isSubmitting}
// >
// Create Service Account
// </Button>
// <Button
// colorSchema="secondary"
// variant="plain"
// onClick={() => handlePopUpClose("addServiceAccount")}
// >
// Cancel
// </Button>
// </div>
// </form>
);
case 1:
return (
@@ -269,27 +277,27 @@ export const OrgServiceAccountsTable = () => {
return (
<div className="w-full">
<div className="mb-4 flex">
<div className="mr-4 flex-1">
<Input
value={searchServiceAccountFilter}
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search service accounts..."
/>
</div>
<div className="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
setStep(0);
reset();
// reset();
handlePopUpOpen("addServiceAccount");
}}
>
Add Service Account
</Button>
</div>
<TableContainer>
<Input
value={searchServiceAccountFilter}
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search service accounts..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Th>Name</Th>
@@ -345,7 +353,7 @@ export const OrgServiceAccountsTable = () => {
isOpen={popUp?.addServiceAccount?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addServiceAccount", isOpen);
reset();
// reset();
}}
>
<ModalContent
@@ -1,5 +1,4 @@
export { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
export { OrgMembersTable } from "./OrgMembersTable";
export { OrgIncidentContactsSection } from "./OrgIncidentContactsSection";
export { OrgMembersSection } from "./OrgMembersSection";
export { OrgNameChangeSection } from "./OrgNameChangeSection";
export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable";
export { OrgServiceAccountsTable } from "./OrgServiceAccountsTable";