feat: added new secret input component and updated toolbar key special prop to innerKey

This commit is contained in:
akhilmhdh
2023-07-27 16:36:34 +05:30
parent b6b3c8a736
commit bca14dd5c4
34 changed files with 2969 additions and 2505 deletions
@@ -53,7 +53,7 @@ export const OrgIncidentContactsTable = ({
isLoading
}: Props) => {
const [searchContact, setSearchContact] = useState("");
const {data: serverDetails } = useFetchServerStatus()
const { data: serverDetails } = useFetchServerStatus();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addContact",
"removeContact",
@@ -98,7 +98,7 @@ export const OrgIncidentContactsTable = ({
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (serverDetails?.emailConfigured){
if (serverDetails?.emailConfigured) {
handlePopUpOpen("addContact");
} else {
handlePopUpOpen("setUpEmail");
@@ -119,7 +119,7 @@ export const OrgIncidentContactsTable = ({
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={2} key="incident-contact" />}
{isLoading && <TableSkeleton columns={2} innerKey="incident-contact" />}
{filteredContacts?.map(({ email }) => (
<Tr key={email}>
<Td className="w-full">{email}</Td>
@@ -1,7 +1,14 @@
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import { faCheck, faCopy, faMagnifyingGlass, faPlus, faTrash, faUsers } from "@fortawesome/free-solid-svg-icons";
import {
faCheck,
faCopy,
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";
@@ -10,7 +17,8 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import {
Button,
DeleteActionModal,
EmailServiceSetupModal, EmptyState,
EmailServiceSetupModal,
EmptyState,
FormControl,
IconButton,
Input,
@@ -29,7 +37,7 @@ import {
Tr,
UpgradePlanModal
} from "@app/components/v2";
import { useOrganization , useWorkspace } from "@app/context";
import { useOrganization, useWorkspace } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import { useGetSSOConfig } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
@@ -47,8 +55,8 @@ type Props = {
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>>
completeInviteLink: string | undefined;
setCompleteInviteLink: Dispatch<SetStateAction<string | undefined>>;
};
const addMemberFormSchema = yup.object({
@@ -76,7 +84,7 @@ export const OrgMembersTable = ({
const { currentOrg } = useOrganization();
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(currentOrg?._id ?? "");
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const {data: serverDetails } = useFetchServerStatus()
const { data: serverDetails } = useFetchServerStatus();
const { workspaces } = useWorkspace();
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -85,7 +93,7 @@ export const OrgMembersTable = ({
"upgradePlan",
"setUpEmail"
] as const);
useEffect(() => {
if (router.query.action === "invite") {
handlePopUpOpen("addMember");
@@ -101,11 +109,11 @@ export const OrgMembersTable = ({
const onAddMember = async ({ email }: TAddMemberForm) => {
await onInviteMember(email);
if (serverDetails?.emailConfigured){
handlePopUpClose("addMember");
}
reset();
if (serverDetails?.emailConfigured) {
handlePopUpClose("addMember");
}
reset();
};
const onRemoveOrgMemberApproved = async () => {
@@ -118,7 +126,7 @@ export const OrgMembersTable = ({
() => members.find(({ user }) => userId === user?._id)?.role === "owner",
[userId, members]
);
const filterdUser = useMemo(
() =>
members.filter(
@@ -163,10 +171,10 @@ export const OrgMembersTable = ({
text: "You cannot invite users when SAML SSO is configured for your organization",
type: "error"
});
return;
}
if (isMoreUserNotAllowed) {
handlePopUpOpen("upgradePlan");
} else {
@@ -190,7 +198,7 @@ export const OrgMembersTable = ({
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={5} key="org-members" />}
{isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
{!isLoading &&
filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
const name = user ? `${user.firstName} ${user.lastName}` : "-";
@@ -219,11 +227,17 @@ export const OrgMembersTable = ({
<SelectItem value="member">member</SelectItem>
</Select>
)}
{((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && (
<Button className='w-40' colorSchema="primary" variant="outline_bg" onClick={() => onInviteMember(email)}>
Resend Invite
</Button>
)}
{(status === "invited" || status === "verified") &&
serverDetails?.emailConfigured && (
<Button
className="w-40"
colorSchema="primary"
variant="outline_bg"
onClick={() => onInviteMember(email)}
>
Resend Invite
</Button>
)}
{status === "completed" && (
<Button
colorSchema="secondary"
@@ -241,18 +255,33 @@ export const OrgMembersTable = ({
</Tag>
))
) : (
<div className='flex flex-row'>
{((status === "invited" || status === "verified") && serverDetails?.emailConfigured)
? <Tag colorSchema="red">This user hasn&apos;t accepted the invite yet</Tag>
: <Tag colorSchema="red">This user isn&apos;t part of any projects yet</Tag>}
{router.query.id !== "undefined" && !((status === "invited" || status === "verified") && serverDetails?.emailConfigured) && <button
type="button"
onClick={() => router.push(`/project/${workspaces[0]?._id}/members`)}
className='text-sm bg-mineshaft w-max px-1.5 py-0.5 hover:bg-primary duration-200 hover:text-black cursor-pointer rounded-sm'
>
<FontAwesomeIcon icon={faPlus} className="mr-1" />
Add to projects
</button>}
<div className="flex flex-row">
{(status === "invited" || status === "verified") &&
serverDetails?.emailConfigured ? (
<Tag colorSchema="red">
This user hasn&apos;t accepted the invite yet
</Tag>
) : (
<Tag colorSchema="red">
This user isn&apos;t part of any projects yet
</Tag>
)}
{router.query.id !== "undefined" &&
!(
(status === "invited" || status === "verified") &&
serverDetails?.emailConfigured
) && (
<button
type="button"
onClick={() =>
router.push(`/project/${workspaces[0]?._id}/members`)
}
className="w-max cursor-pointer rounded-sm bg-mineshaft px-1.5 py-0.5 text-sm duration-200 hover:bg-primary hover:text-black"
>
<FontAwesomeIcon icon={faPlus} className="mr-1" />
Add to projects
</button>
)}
</div>
)}
</Td>
@@ -282,67 +311,73 @@ export const OrgMembersTable = ({
isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addMember", isOpen);
setCompleteInviteLink(undefined)
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"}
{!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 &&
{!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>
}
<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
@@ -363,4 +398,4 @@ export const OrgMembersTable = ({
/>
</div>
);
};
};
@@ -1,14 +1,15 @@
import { useEffect, useMemo,useState } from "react";
import { Controller,useForm } from "react-hook-form";
import { useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import {
faCheck,
faCopy,
faMagnifyingGlass,
faPencil,
faPlus,
faServer,
faTrash} from "@fortawesome/free-solid-svg-icons";
import {
faCheck,
faCopy,
faMagnifyingGlass,
faPencil,
faPlus,
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";
@@ -37,9 +38,10 @@ import {
import { useOrganization, useWorkspace } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
useCreateServiceAccount,
useDeleteServiceAccount,
useGetServiceAccounts} from "@app/hooks/api";
useCreateServiceAccount,
useDeleteServiceAccount,
useGetServiceAccounts
} from "@app/hooks/api";
const serviceAccountExpiration = [
{ label: "1 Day", value: 86400 },
@@ -51,317 +53,315 @@ const serviceAccountExpiration = [
];
const addServiceAccountFormSchema = yup.object({
name: yup.string().required().label("Name").trim(),
expiresIn: yup.string().required().label("Service Account Expiration")
name: yup.string().required().label("Name").trim(),
expiresIn: yup.string().required().label("Service Account Expiration")
});
type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
export const OrgServiceAccountsTable = () => {
const router = useRouter();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?._id || "";
const [step, setStep] = useState(0);
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 [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addServiceAccount",
"removeServiceAccount",
] as const);
const router = useRouter();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId);
const createServiceAccount = useCreateServiceAccount();
const removeServiceAccount = useDeleteServiceAccount();
useEffect(() => {
let timer: NodeJS.Timeout;
if (isAccessKeyCopied) {
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
}
const orgId = currentOrg?._id || "";
const [step, setStep] = useState(0);
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 [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addServiceAccount",
"removeServiceAccount"
] as const);
if (isPublicKeyCopied) {
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
}
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
useGetServiceAccounts(orgId);
if (isPrivateKeyCopied) {
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
}
const createServiceAccount = useCreateServiceAccount();
const removeServiceAccount = useDeleteServiceAccount();
return () => clearTimeout(timer);
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
if (!currentOrg?._id) return;
const keyPair = generateKeyPair();
setPublicKey(keyPair.publicKey);
setPrivateKey(keyPair.privateKey);
const serviceAccountDetails = await createServiceAccount.mutateAsync({
name,
organizationId: currentOrg?._id,
publicKey: keyPair.publicKey,
expiresIn: Number(expiresIn)
});
setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
setStep(1);
reset();
useEffect(() => {
let timer: NodeJS.Timeout;
if (isAccessKeyCopied) {
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
}
const onRemoveServiceAccount = async () => {
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
await removeServiceAccount.mutateAsync(serviceAccountId);
handlePopUpClose("removeServiceAccount");
if (isPublicKeyCopied) {
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
}
const filteredServiceAccounts = useMemo(
() =>
serviceAccounts.filter(
({ name }) =>
name.toLowerCase().includes(searchServiceAccountFilter)
),
[serviceAccounts, searchServiceAccountFilter]
);
const renderStep = (stepToRender: number) => {
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>
);
case 1:
return (
<>
<p>Access Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
<p className="mr-4 break-all">{accessKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(accessKey);
setIsAccessKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isAccessKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
<p className="mt-4">Public Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
<p className="mr-4 break-all">{publicKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(publicKey);
setIsPublicKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isPublicKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
<p className="mt-4">Private Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
<p className="mr-4 break-all">{privateKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(privateKey);
setIsPrivateKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isPrivateKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
</>
);
default:
return <div />
}
if (isPrivateKeyCopied) {
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
}
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>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
setStep(0);
reset();
handlePopUpOpen("addServiceAccount");
}}
>
Add Service Account
</Button>
</div>
<TableContainer>
<Table>
<THead>
<Th>Name</Th>
<Th className="w-full">Valid Until</Th>
<Th aria-label="actions" />
</THead>
<TBody>
{isServiceAccountsLoading && <TableSkeleton columns={5} key="org-service-accounts" />}
{!isServiceAccountsLoading && (
filteredServiceAccounts.map(({
name,
expiresAt,
_id: serviceAccountId
}) => {
return (
<Tr key={`org-service-account-${serviceAccountId}`}>
<Td>{name}</Td>
<Td>{new Date(expiresAt).toUTCString()}</Td>
<Td>
<div className="flex">
<IconButton
ariaLabel="edit"
colorSchema="secondary"
onClick={() => {
if (currentWorkspace?._id) {
router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`);
}
}}
className="mr-2"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</Td>
</Tr>
);
})
)}
</TBody>
</Table>
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
<EmptyState title="No service accounts found" icon={faServer} />
)}
</TableContainer>
<Modal
isOpen={popUp?.addServiceAccount?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addServiceAccount", isOpen);
reset();
}}
>
<ModalContent
title="Add Service Account"
subTitle="A service account represents a machine identity such as a VM or application client."
>
{renderStep(step)}
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeServiceAccount.isOpen}
deleteKey="remove"
title="Do you want to remove this service account from the org?"
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
onDeleteApproved={onRemoveServiceAccount}
return () => clearTimeout(timer);
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
if (!currentOrg?._id) return;
const keyPair = generateKeyPair();
setPublicKey(keyPair.publicKey);
setPrivateKey(keyPair.privateKey);
const serviceAccountDetails = await createServiceAccount.mutateAsync({
name,
organizationId: currentOrg?._id,
publicKey: keyPair.publicKey,
expiresIn: Number(expiresIn)
});
setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
setStep(1);
reset();
};
const onRemoveServiceAccount = async () => {
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
await removeServiceAccount.mutateAsync(serviceAccountId);
handlePopUpClose("removeServiceAccount");
};
const filteredServiceAccounts = useMemo(
() =>
serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)),
[serviceAccounts, searchServiceAccountFilter]
);
const renderStep = (stepToRender: number) => {
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>
);
case 1:
return (
<>
<p>Access Key</p>
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{accessKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(accessKey);
setIsAccessKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isAccessKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
<p className="mt-4">Public Key</p>
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{publicKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(publicKey);
setIsPublicKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isPublicKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
<p className="mt-4">Private Key</p>
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{privateKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(privateKey);
setIsPrivateKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isPrivateKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
</>
);
default:
return <div />;
}
};
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>
);
}
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
setStep(0);
reset();
handlePopUpOpen("addServiceAccount");
}}
>
Add Service Account
</Button>
</div>
<TableContainer>
<Table>
<THead>
<Th>Name</Th>
<Th className="w-full">Valid Until</Th>
<Th aria-label="actions" />
</THead>
<TBody>
{isServiceAccountsLoading && (
<TableSkeleton columns={5} innerKey="org-service-accounts" />
)}
{!isServiceAccountsLoading &&
filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => {
return (
<Tr key={`org-service-account-${serviceAccountId}`}>
<Td>{name}</Td>
<Td>{new Date(expiresAt).toUTCString()}</Td>
<Td>
<div className="flex">
<IconButton
ariaLabel="edit"
colorSchema="secondary"
onClick={() => {
if (currentWorkspace?._id) {
router.push(
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
);
}
}}
className="mr-2"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() =>
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
<EmptyState title="No service accounts found" icon={faServer} />
)}
</TableContainer>
<Modal
isOpen={popUp?.addServiceAccount?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addServiceAccount", isOpen);
reset();
}}
>
<ModalContent
title="Add Service Account"
subTitle="A service account represents a machine identity such as a VM or application client."
>
{renderStep(step)}
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeServiceAccount.isOpen}
deleteKey="remove"
title="Do you want to remove this service account from the org?"
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
onDeleteApproved={onRemoveServiceAccount}
/>
</div>
);
};
@@ -10,90 +10,94 @@ import {
Td,
Th,
THead,
Tr} from "@app/components/v2";
Tr
} from "@app/components/v2";
import timeSince from "@app/ee/utilities/timeSince";
import getRisksByOrganization, { GitRisks } from "@app/pages/api/secret-scanning/getRisksByOrganization";
import getRisksByOrganization, {
GitRisks
} from "@app/pages/api/secret-scanning/getRisksByOrganization";
import { RiskStatusSelection } from "./RiskStatusSelection";
export const SecretScanningLogsTable = () => {
const [isLoading, setIsLoading] = useState(false);
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
useEffect(() => {
const fetchRisks = async () => {
setIsLoading(true);
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")))
setGitRisks(risks);
setIsLoading(false);
}
useEffect(() => {
const fetchRisks = async () => {
setIsLoading(true);
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")));
setGitRisks(risks);
setIsLoading(false);
};
fetchRisks();
},[])
fetchRisks();
}, []);
return (
<TableContainer className="mt-8">
<Table>
<THead>
<Tr>
<Th className="flex-1">Date</Th>
<Th className="flex-1">Secret Type</Th>
<Th className="flex-1">View Risk</Th>
<Th className="flex-1">Info</Th>
<Th className="flex-1">Status</Th>
<Th className="flex-1">Action</Th>
<Th className="w-5" />
return (
<TableContainer className="mt-8">
<Table>
<THead>
<Tr>
<Th className="flex-1">Date</Th>
<Th className="flex-1">Secret Type</Th>
<Th className="flex-1">View Risk</Th>
<Th className="flex-1">Info</Th>
<Th className="flex-1">Status</Th>
<Th className="flex-1">Action</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading &&
gitRisks &&
gitRisks?.map((risk) => {
return (
<Tr key={risk.ruleID} className="h-10">
<Td>{timeSince(new Date(risk.createdAt))}</Td>
<Td>{risk.ruleID}</Td>
<Td>
<a
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
target="_blank"
className="text-red-500"
rel="noreferrer"
>
View Exposed Secret
</a>
</Td>
<Td>
<div className="font-bold">
<a href={`https://github.com/${risk.repositoryFullName}`}>
{risk.repositoryFullName}
</a>
</div>
<div className="text-xs">
<span>{risk.file}</span>
<br />
<br />
<span className="font-bold">{risk.author}</span>
<br />
<span>{risk.email}</span>
</div>
</Td>
<Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td>
<Td>
<RiskStatusSelection riskId={risk._id} currentSelection={risk.status} />
</Td>
</Tr>
</THead>
<TBody>
{!isLoading && gitRisks && gitRisks?.map((risk) => {
return (
<Tr key={risk.ruleID} className="h-10">
<Td>{timeSince(new Date(risk.createdAt))}</Td>
<Td>{risk.ruleID}</Td>
<Td>
<a
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
target="_blank"
className="text-red-500" rel="noreferrer"
>
View Exposed Secret
</a>
</Td>
<Td>
<div className="font-bold">
<a href={`https://github.com/${risk.repositoryFullName}`}>
{risk.repositoryFullName}
</a>
</div>
<div className="text-xs">
<span>{risk.file}</span><br/>
<br/>
<span className="font-bold">{risk.author}</span><br/>
<span>{risk.email}</span>
</div>
</Td>
<Td>{risk.isResolved ? "Resolved" : "Needs Attention"}</Td>
<Td>
<RiskStatusSelection riskId={risk._id} currentSelection={risk.status}/>
</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={7} key="gitRisks" />}
{!isLoading && gitRisks && gitRisks?.length === 0 && (
<Tr>
<Td colSpan={7}>
<EmptyState
title="No risks detected."
icon={faCheck}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
);
})}
{isLoading && <TableSkeleton columns={7} innerKey="gitRisks" />}
{!isLoading && gitRisks && gitRisks?.length === 0 && (
<Tr>
<Td colSpan={7}>
<EmptyState title="No risks detected." icon={faCheck} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -1,4 +1,4 @@
import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons";
import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
@@ -10,78 +10,63 @@ import {
Td,
Th,
THead,
Tr} from "@app/components/v2";
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgPlanTable
} from "@app/hooks/api";
import { useGetOrgPlanTable } from "@app/hooks/api";
export const CurrentPlanSection = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? "");
const displayCell = (value: null | number | string | boolean) => {
if (value === null) return "-";
if (typeof value === "boolean") {
if (value) return (
<FontAwesomeIcon
icon={faCircleCheck}
color='#2ecc71'
/>
);
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? "");
return (
<FontAwesomeIcon
icon={faCircleXmark}
color='#e74c3c'
/>
);
}
return value;
const displayCell = (value: null | number | string | boolean) => {
if (value === null) return "-";
if (typeof value === "boolean") {
if (value) return <FontAwesomeIcon icon={faCircleCheck} color="#2ecc71" />;
return <FontAwesomeIcon icon={faCircleXmark} color="#e74c3c" />;
}
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<h2 className="text-xl font-semibold flex-1 text-white mb-8">Current Usage</h2>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/3">Feature</Th>
<Th className="w-1/3">Allowed</Th>
<Th className="w-1/3">Used</Th>
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.rows?.length > 0 && data.rows.map(({
name,
allowed,
used
}) => {
return (
<Tr key={`current-plan-row-${name}`} className="h-12">
<Td>{name}</Td>
<Td>{displayCell(allowed)}</Td>
<Td>{used}</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={5} key="invoices" />}
{!isLoading && data && data?.rows?.length === 0 && (
<Tr>
<Td colSpan={3}>
<EmptyState
title="No plan details found"
icon={faFileInvoice}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
);
}
return value;
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Current Usage</h2>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/3">Feature</Th>
<Th className="w-1/3">Allowed</Th>
<Th className="w-1/3">Used</Th>
</Tr>
</THead>
<TBody>
{!isLoading &&
data &&
data?.rows?.length > 0 &&
data.rows.map(({ name, allowed, used }) => {
return (
<Tr key={`current-plan-row-${name}`} className="h-12">
<Td>{name}</Td>
<Td>{displayCell(allowed)}</Td>
<Td>{used}</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={5} innerKey="invoices" />}
{!isLoading && data && data?.rows?.length === 0 && (
<Tr>
<Td colSpan={3}>
<EmptyState title="No plan details found" icon={faFileInvoice} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
);
};
@@ -1,4 +1,4 @@
import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons";
import { faCircleCheck, faCircleXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
@@ -11,166 +11,129 @@ import {
Td,
Th,
THead,
Tr,
Tr
} from "@app/components/v2";
import { useOrganization,useSubscription } from "@app/context";
import {
useCreateCustomerPortalSession,
useGetOrgPlansTable} from "@app/hooks/api";
import { useOrganization, useSubscription } from "@app/context";
import { useCreateCustomerPortalSession, useGetOrgPlansTable } from "@app/hooks/api";
type Props = {
billingCycle: "monthly" | "yearly"
}
billingCycle: "monthly" | "yearly";
};
export const ManagePlansTable = ({
export const ManagePlansTable = ({ billingCycle }: Props) => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({
organizationId: currentOrg?._id ?? "",
billingCycle
}: Props) => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({
organizationId: currentOrg?._id ?? "",
billingCycle
});
const createCustomerPortalSession = useCreateCustomerPortalSession();
});
const createCustomerPortalSession = useCreateCustomerPortalSession();
const displayCell = (value: null | number | string | boolean) => {
if (value === null) return "Unlimited";
if (typeof value === "boolean") {
if (value) return (
<FontAwesomeIcon
icon={faCircleCheck}
color='#2ecc71'
/>
);
const displayCell = (value: null | number | string | boolean) => {
if (value === null) return "Unlimited";
return (
<FontAwesomeIcon
icon={faCircleXmark}
color='#e74c3c'
/>
);
}
return value;
if (typeof value === "boolean") {
if (value) return <FontAwesomeIcon icon={faCircleCheck} color="#2ecc71" />;
return <FontAwesomeIcon icon={faCircleXmark} color="#e74c3c" />;
}
return (
<TableContainer>
<Table>
<THead>
{subscription && !isTableDataLoading && tableData && (
<Tr>
<Th className="">Feature / Limit</Th>
{tableData.head.map(({
name,
priceLine
}) => {
return (
<Th
key={`plans-feature-head-${billingCycle}-${name}`}
className="text-center flex-1"
>
<p>{name}</p>
<p>{priceLine}</p>
</Th>
);
})}
</Tr>
)}
</THead>
<TBody>
{subscription && !isTableDataLoading && tableData && tableData.rows.map(({
name,
starter,
team,
pro,
enterprise
}) => {
return (
<Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}>
<Td>{displayCell(name)}</Td>
<Td className="text-center">
{displayCell(starter)}
</Td>
<Td className="text-center">
{displayCell(team)}
</Td>
<Td className="text-center">
{displayCell(pro)}
</Td>
<Td className="text-center">
{displayCell(enterprise)}
</Td>
</Tr>
);
})}
{isTableDataLoading && <TableSkeleton columns={5} key="cloud-products" />}
{!isTableDataLoading && tableData?.rows.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No cloud product details found"
icon={faFileInvoice}
/>
</Td>
</Tr>
)}
{subscription && !isTableDataLoading && tableData && (
<Tr className="h-12">
<Td />
{tableData.head.map(({
slug,
tier
}) => {
const isCurrentPlan = slug === subscription.slug;
let subscriptionText = "Upgrade";
if (subscription.tier > tier) {
subscriptionText = "Downgrade"
}
if (tier === 3) {
subscriptionText = "Contact sales"
}
return value;
};
return isCurrentPlan ? (
<Td>
<Button
colorSchema="secondary"
className="w-full"
isDisabled
>
Current
</Button>
</Td>
) : (
<Td>
<Button
onClick={async () => {
if (!currentOrg?._id) return;
if (tier !== 3) {
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
window.location.href = url;
return;
}
window.location.href = "https://infisical.com/scheduledemo";
}}
color="mineshaft"
className="w-full"
>
{subscriptionText}
</Button>
</Td>
);
})}
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
return (
<TableContainer>
<Table>
<THead>
{subscription && !isTableDataLoading && tableData && (
<Tr>
<Th className="">Feature / Limit</Th>
{tableData.head.map(({ name, priceLine }) => {
return (
<Th
key={`plans-feature-head-${billingCycle}-${name}`}
className="flex-1 text-center"
>
<p>{name}</p>
<p>{priceLine}</p>
</Th>
);
})}
</Tr>
)}
</THead>
<TBody>
{subscription &&
!isTableDataLoading &&
tableData &&
tableData.rows.map(({ name, starter, team, pro, enterprise }) => {
return (
<Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}>
<Td>{displayCell(name)}</Td>
<Td className="text-center">{displayCell(starter)}</Td>
<Td className="text-center">{displayCell(team)}</Td>
<Td className="text-center">{displayCell(pro)}</Td>
<Td className="text-center">{displayCell(enterprise)}</Td>
</Tr>
);
})}
{isTableDataLoading && <TableSkeleton columns={5} innerKey="cloud-products" />}
{!isTableDataLoading && tableData?.rows.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No cloud product details found" icon={faFileInvoice} />
</Td>
</Tr>
)}
{subscription && !isTableDataLoading && tableData && (
<Tr className="h-12">
<Td />
{tableData.head.map(({ slug, tier }) => {
const isCurrentPlan = slug === subscription.slug;
let subscriptionText = "Upgrade";
if (subscription.tier > tier) {
subscriptionText = "Downgrade";
}
if (tier === 3) {
subscriptionText = "Contact sales";
}
return isCurrentPlan ? (
<Td>
<Button colorSchema="secondary" className="w-full" isDisabled>
Current
</Button>
</Td>
) : (
<Td>
<Button
onClick={async () => {
if (!currentOrg?._id) return;
if (tier !== 3) {
const { url } = await createCustomerPortalSession.mutateAsync(
currentOrg._id
);
window.location.href = url;
return;
}
window.location.href = "https://infisical.com/scheduledemo";
}}
color="mineshaft"
className="w-full"
>
{subscriptionText}
</Button>
</Td>
);
})}
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -14,78 +14,68 @@ import {
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useDeleteOrgPmtMethod,
useGetOrgPmtMethods
} from "@app/hooks/api";
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
export const PmtMethodsTable = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? "");
const deleteOrgPmtMethod = useDeleteOrgPmtMethod();
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? "");
const deleteOrgPmtMethod = useDeleteOrgPmtMethod();
const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => {
if (!currentOrg?._id) return;
await deleteOrgPmtMethod.mutateAsync({
organizationId: currentOrg._id,
pmtMethodId
});
}
const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => {
if (!currentOrg?._id) return;
await deleteOrgPmtMethod.mutateAsync({
organizationId: currentOrg._id,
pmtMethodId
});
};
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="flex-1">Brand</Th>
<Th className="flex-1">Type</Th>
<Th className="flex-1">Last 4 Digits</Th>
<Th className="flex-1">Expiration</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.length > 0 && data.map(({
_id,
brand,
exp_month,
exp_year,
funding,
last4
}) => (
<Tr key={`pmt-method-${_id}`} className="h-10">
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
<Td>{last4}</Td>
<Td>{`${exp_month}/${exp_year}`}</Td>
<Td>
<IconButton
onClick={async () => {
await handleDeletePmtMethodBtnClick(_id);
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
))}
{isLoading && <TableSkeleton columns={5} key="pmt-methods" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No payment methods on file"
icon={faCreditCard}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="flex-1">Brand</Th>
<Th className="flex-1">Type</Th>
<Th className="flex-1">Last 4 Digits</Th>
<Th className="flex-1">Expiration</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading &&
data &&
data?.length > 0 &&
data.map(({ _id, brand, exp_month, exp_year, funding, last4 }) => (
<Tr key={`pmt-method-${_id}`} className="h-10">
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
<Td>{last4}</Td>
<Td>{`${exp_month}/${exp_year}`}</Td>
<Td>
<IconButton
onClick={async () => {
await handleDeletePmtMethodBtnClick(_id);
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
))}
{isLoading && <TableSkeleton columns={5} innerKey="pmt-methods" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No payment methods on file" icon={faCreditCard} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -1,4 +1,4 @@
import { faFileInvoice,faXmark } from "@fortawesome/free-solid-svg-icons";
import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
@@ -11,127 +11,120 @@ import {
Td,
Th,
THead,
Tr,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useDeleteOrgTaxId,
useGetOrgTaxIds
} from "@app/hooks/api";
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
const taxIDTypeLabelMap: { [key: string]: string } = {
"au_abn": "Australia ABN",
"au_arn": "Australia ARN",
"bg_uic": "Bulgaria UIC",
"br_cnpj": "Brazil CNPJ",
"br_cpf": "Brazil CPF",
"ca_bn": "Canada BN",
"ca_gst_hst": "Canada GST/HST",
"ca_pst_bc": "Canada PST BC",
"ca_pst_mb": "Canada PST MB",
"ca_pst_sk": "Canada PST SK",
"ca_qst": "Canada QST",
"ch_vat": "Switzerland VAT",
"cl_tin": "Chile TIN",
"eg_tin": "Egypt TIN",
"es_cif": "Spain CIF",
"eu_oss_vat": "EU OSS VAT",
"eu_vat": "EU VAT",
"gb_vat": "GB VAT",
"ge_vat": "Georgia VAT",
"hk_br": "Hong Kong BR",
"hu_tin": "Hungary TIN",
"id_npwp": "Indonesia NPWP",
"il_vat": "Israel VAT",
"in_gst": "India GST",
"is_vat": "Iceland VAT",
"jp_cn": "Japan CN",
"jp_rn": "Japan RN",
"jp_trn": "Japan TRN",
"ke_pin": "Kenya PIN",
"kr_brn": "South Korea BRN",
"li_uid": "Liechtenstein UID",
"mx_rfc": "Mexico RFC",
"my_frp": "Malaysia FRP",
"my_itn": "Malaysia ITN",
"my_sst": "Malaysia SST",
"no_vat": "Norway VAT",
"nz_gst": "New Zealand GST",
"ph_tin": "Philippines TIN",
"ru_inn": "Russia INN",
"ru_kpp": "Russia KPP",
"sa_vat": "Saudi Arabia VAT",
"sg_gst": "Singapore GST",
"sg_uen": "Singapore UEN",
"si_tin": "Slovenia TIN",
"th_vat": "Thailand VAT",
"tr_tin": "Turkey TIN",
"tw_vat": "Taiwan VAT",
"ua_vat": "Ukraine VAT",
"us_ein": "US EIN",
"za_vat": "South Africa VAT"
au_abn: "Australia ABN",
au_arn: "Australia ARN",
bg_uic: "Bulgaria UIC",
br_cnpj: "Brazil CNPJ",
br_cpf: "Brazil CPF",
ca_bn: "Canada BN",
ca_gst_hst: "Canada GST/HST",
ca_pst_bc: "Canada PST BC",
ca_pst_mb: "Canada PST MB",
ca_pst_sk: "Canada PST SK",
ca_qst: "Canada QST",
ch_vat: "Switzerland VAT",
cl_tin: "Chile TIN",
eg_tin: "Egypt TIN",
es_cif: "Spain CIF",
eu_oss_vat: "EU OSS VAT",
eu_vat: "EU VAT",
gb_vat: "GB VAT",
ge_vat: "Georgia VAT",
hk_br: "Hong Kong BR",
hu_tin: "Hungary TIN",
id_npwp: "Indonesia NPWP",
il_vat: "Israel VAT",
in_gst: "India GST",
is_vat: "Iceland VAT",
jp_cn: "Japan CN",
jp_rn: "Japan RN",
jp_trn: "Japan TRN",
ke_pin: "Kenya PIN",
kr_brn: "South Korea BRN",
li_uid: "Liechtenstein UID",
mx_rfc: "Mexico RFC",
my_frp: "Malaysia FRP",
my_itn: "Malaysia ITN",
my_sst: "Malaysia SST",
no_vat: "Norway VAT",
nz_gst: "New Zealand GST",
ph_tin: "Philippines TIN",
ru_inn: "Russia INN",
ru_kpp: "Russia KPP",
sa_vat: "Saudi Arabia VAT",
sg_gst: "Singapore GST",
sg_uen: "Singapore UEN",
si_tin: "Slovenia TIN",
th_vat: "Thailand VAT",
tr_tin: "Turkey TIN",
tw_vat: "Taiwan VAT",
ua_vat: "Ukraine VAT",
us_ein: "US EIN",
za_vat: "South Africa VAT"
};
export const TaxIDTable = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? "");
const deleteOrgTaxId = useDeleteOrgTaxId();
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? "");
const deleteOrgTaxId = useDeleteOrgTaxId();
const handleDeleteTaxIdBtnClick = async (taxId: string) => {
if (!currentOrg?._id) return;
await deleteOrgTaxId.mutateAsync({
organizationId: currentOrg._id,
taxId
});
}
const handleDeleteTaxIdBtnClick = async (taxId: string) => {
if (!currentOrg?._id) return;
await deleteOrgTaxId.mutateAsync({
organizationId: currentOrg._id,
taxId
});
};
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="flex-1">Type</Th>
<Th className="flex-1">Value</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.length > 0 && data.map(({
_id,
type,
value
}) => (
<Tr key={`tax-id-${_id}`} className="h-10">
<Td>{taxIDTypeLabelMap[type]}</Td>
<Td>{value}</Td>
<Td>
<IconButton
onClick={async () => {
await handleDeleteTaxIdBtnClick(_id);
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
))}
{isLoading && <TableSkeleton columns={3} key="tax-ids" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No Tax IDs on file"
icon={faFileInvoice}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="flex-1">Type</Th>
<Th className="flex-1">Value</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading &&
data &&
data?.length > 0 &&
data.map(({ _id, type, value }) => (
<Tr key={`tax-id-${_id}`} className="h-10">
<Td>{taxIDTypeLabelMap[type]}</Td>
<Td>{value}</Td>
<Td>
<IconButton
onClick={async () => {
await handleDeleteTaxIdBtnClick(_id);
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
))}
{isLoading && <TableSkeleton columns={3} innerKey="tax-ids" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No Tax IDs on file" icon={faFileInvoice} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -14,76 +14,67 @@ import {
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgInvoices
} from "@app/hooks/api";
import { useGetOrgInvoices } from "@app/hooks/api";
export const InvoicesTable = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? "");
return (
<TableContainer className="mt-8">
<Table>
<THead>
<Tr>
<Th className="flex-1">Invoice #</Th>
<Th className="flex-1">Date</Th>
<Th className="flex-1">Status</Th>
<Th className="flex-1">Amount</Th>
<Th className="w-5" />
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? "");
return (
<TableContainer className="mt-8">
<Table>
<THead>
<Tr>
<Th className="flex-1">Invoice #</Th>
<Th className="flex-1">Date</Th>
<Th className="flex-1">Status</Th>
<Th className="flex-1">Amount</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{!isLoading &&
data &&
data?.length > 0 &&
data.map(({ _id, created, paid, number, total, invoice_pdf }) => {
const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD"
});
const createdDate = new Date(created * 1000);
const day: number = createdDate.getDate();
const month: number = createdDate.getMonth() + 1;
const year: number = createdDate.getFullYear();
const formattedDate: string = `${day}/${month}/${year}`;
return (
<Tr key={`invoice-${_id}`} className="h-10">
<Td>{number}</Td>
<Td>{formattedDate}</Td>
<Td>{paid ? "Paid" : "Not Paid"}</Td>
<Td>{formattedTotal}</Td>
<Td>
<IconButton
onClick={async () => window.open(invoice_pdf)}
size="lg"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Td>
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.length > 0 && data.map(({
_id,
created,
paid,
number,
total,
invoice_pdf
}) => {
const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD",
});
const createdDate = new Date(created * 1000);
const day: number = createdDate.getDate();
const month: number = createdDate.getMonth() + 1;
const year: number = createdDate.getFullYear();
const formattedDate: string = `${day}/${month}/${year}`;
return (
<Tr key={`invoice-${_id}`} className="h-10">
<Td>{number}</Td>
<Td>{formattedDate}</Td>
<Td>{paid ? "Paid" : "Not Paid"}</Td>
<Td>{formattedTotal}</Td>
<Td>
<IconButton
onClick={async () => window.open(invoice_pdf)}
size="lg"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={5} key="invoices" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No invoices on file"
icon={faFileInvoice}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
);
})}
{isLoading && <TableSkeleton columns={5} innerKey="invoices" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No invoices on file" icon={faFileInvoice} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -1,407 +1,412 @@
import { useState } from "react";
import { Controller,useForm } from "react-hook-form";
import {
faKey,
faMagnifyingGlass,
faPlus,
faTrash} from "@fortawesome/free-solid-svg-icons";
import { Controller, useForm } from "react-hook-form";
import { faKey, 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 {
decryptAssymmetric,
encryptAssymmetric,
verifyPrivateKey} from "@app/components/utilities/cryptography/crypto";
import {
Button,
Checkbox,
DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr} from "@app/components/v2";
decryptAssymmetric,
encryptAssymmetric,
verifyPrivateKey
} from "@app/components/utilities/cryptography/crypto";
import {
Button,
Checkbox,
DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalClose,
ModalContent,
Select,
SelectItem,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import {
useCreateServiceAccountProjectLevelPermission,
useDeleteServiceAccountProjectLevelPermission,
useGetServiceAccountById,
useGetServiceAccountProjectLevelPermissions,
useGetUserWorkspaces
useCreateServiceAccountProjectLevelPermission,
useDeleteServiceAccountProjectLevelPermission,
useGetServiceAccountById,
useGetServiceAccountProjectLevelPermissions,
useGetUserWorkspaces
} from "@app/hooks/api";
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
const createProjectLevelPermissionSchema = yup.object({
privateKey: yup.string().required().label("Private Key"),
workspace: yup.string().required().label("Workspace"),
environment: yup.string().required().label("Environment"),
permissions: yup.object().shape({
read: yup.boolean().required(),
write: yup.boolean().required()
}).defined().required()
privateKey: yup.string().required().label("Private Key"),
workspace: yup.string().required().label("Workspace"),
environment: yup.string().required().label("Environment"),
permissions: yup
.object()
.shape({
read: yup.boolean().required(),
write: yup.boolean().required()
})
.defined()
.required()
});
type CreateProjectLevelPermissionForm = yup.InferType<typeof createProjectLevelPermissionSchema>;
type Props = {
serviceAccountId: string;
}
serviceAccountId: string;
};
export const SAProjectLevelPermissionsTable = ({
serviceAccountId
}: Props): JSX.Element => {
const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces();
const [searchPermissions, setSearchPermissions] = useState("");
export const SAProjectLevelPermissionsTable = ({ serviceAccountId }: Props): JSX.Element => {
const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces();
const [searchPermissions, setSearchPermissions] = useState("");
const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } = useGetServiceAccountProjectLevelPermissions(serviceAccountId);
const createServiceAccountProjectLevelPermission = useCreateServiceAccountProjectLevelPermission();
const deleteServiceAccountProjectLevelPermission = useDeleteServiceAccountProjectLevelPermission();
const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } =
useGetServiceAccountProjectLevelPermissions(serviceAccountId);
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addProjectLevelPermission",
"removeProjectLevelPermission",
] as const);
const [, setSelectedWorkspace] = useState<undefined | string>(undefined);
const createServiceAccountProjectLevelPermission =
useCreateServiceAccountProjectLevelPermission();
const deleteServiceAccountProjectLevelPermission =
useDeleteServiceAccountProjectLevelPermission();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<CreateProjectLevelPermissionForm>({ resolver: yupResolver(createProjectLevelPermissionSchema) })
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addProjectLevelPermission",
"removeProjectLevelPermission"
] as const);
const onAddProjectLevelPermission = async ({
privateKey,
workspace,
environment,
permissions: { read, write }
}: CreateProjectLevelPermissionForm) => {
// TODO: clean up / modularize this function
if (!serviceAccount) return;
const { latestKey } = await getLatestFileKey({
workspaceId: workspace
});
const [, setSelectedWorkspace] = useState<undefined | string>(undefined);
verifyPrivateKey({
privateKey,
publicKey: serviceAccount.publicKey
});
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<CreateProjectLevelPermissionForm>({
resolver: yupResolver(createProjectLevelPermissionSchema)
});
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey: serviceAccount.publicKey,
privateKey
});
await createServiceAccountProjectLevelPermission.mutateAsync({
serviceAccountId,
workspaceId: workspace,
environment,
read,
write,
encryptedKey: ciphertext,
nonce
});
handlePopUpClose("addProjectLevelPermission");
}
const onRemoveProjectLevelPermission = async () => {
const serviceAccountWorkspacePermissionId = (popUp?.removeProjectLevelPermission?.data as { _id: string })?._id;
await deleteServiceAccountProjectLevelPermission.mutateAsync({
serviceAccountId,
serviceAccountWorkspacePermissionId
});
handlePopUpClose("removeProjectLevelPermission");
}
const onAddProjectLevelPermission = async ({
privateKey,
workspace,
environment,
permissions: { read, write }
}: CreateProjectLevelPermissionForm) => {
// TODO: clean up / modularize this function
return (
<div className="w-full bg-white/5 p-6">
<p className="mb-4 text-xl font-semibold">Project-Level Permissions</p>
<div className="mb-4 flex">
<div className="mr-4 flex-1">
<Input
value={searchPermissions}
onChange={(e) => setSearchPermissions(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search service account project-level permissions..."
/>
</div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("addProjectLevelPermission")
reset();
}}
>
Add Permission
</Button>
</div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Project</Th>
<Th>Environment</Th>
<Th>Read</Th>
<Th>Write</Th>
<Th aria-label="actions" />
</Tr>
</THead>
<TBody>
{isPermissionsLoading && <TableSkeleton columns={6} key="service-account-project-level-permissions" />}
{!isPermissionsLoading && serviceAccountWorkspacePermissions && (
serviceAccountWorkspacePermissions.map(({
_id,
workspace,
environment,
read,
write
}) => {
const environmentName = (workspace.environments.find((env) => env.slug === environment))?.name;
return (
<Tr key={`service-account-project-level-permission-${_id}`} className="w-full">
<Td>{workspace.name}</Td>
<Td>{environmentName}</Td>
<Td>
<Checkbox
id="isReadPermissionEnabled"
isChecked={read}
isDisabled
>{/**/}</Checkbox>
</Td>
<Td>
<Checkbox
id="isWritePermissionEnabled"
isChecked={write}
isDisabled
>{/**/}</Checkbox>
</Td>
<Td>
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen("removeProjectLevelPermission", { _id })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Td>
</Tr>
);
})
)}
{!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && (
<Tr>
<Td colSpan={7} className="py-6 text-center text-bunker-400">
<EmptyState title="No permissions found" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
<Modal
isOpen={popUp?.addProjectLevelPermission?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addProjectLevelPermission", isOpen);
}}
>
<ModalContent
title="Add a Project-Level Permission"
subTitle="The service account will be granted scoped access to the specified project and environment"
>
<form onSubmit={handleSubmit(onAddProjectLevelPermission)}>
{!isUserWorkspacesLoading && userWorkspaces && (
<>
<Controller
control={control}
defaultValue=""
name="privateKey"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Service Account Private Key"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="workspace"
defaultValue={String(userWorkspaces?.[0]?._id)}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Project"
errorText={error?.message}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
onChange(e);
setSelectedWorkspace(e);
}}
className="w-full border border-mine-shaft-500"
>
{userWorkspaces && userWorkspaces.length > 0 ? (
userWorkspaces.map((userWorkspace) => {
return (
<SelectItem value={userWorkspace._id} key={`project-${userWorkspace._id}`}>
{userWorkspace.name}
</SelectItem>
);
})
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="environment"
defaultValue={String(userWorkspaces?.[0]?.environments?.[0]?.slug)}
render={({ field: { onChange, ...field } }) => {
/* eslint-disable-next-line no-underscore-dangle */
const environments = userWorkspaces?.find((userWorkspace) => userWorkspace._id === control?._formValues?.workspace)?.environments ?? [];
return (
<FormControl
label="Environment"
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full border border-mine-shaft-500"
>
{environments.length > 0 ? (
environments.map((environment) => {
return (
<SelectItem value={environment.slug} key={`environment-${environment.slug}`}>
{environment.name}
</SelectItem>
);
})
) : (
<SelectItem value="none" key="target-app-none">
No environments found
</SelectItem>
)}
</Select>
</FormControl>
);
}}
/>
</>
)}
<Controller
control={control}
name="permissions"
defaultValue={{
read: true,
write: false
}}
render={({ field: { onChange, value }, fieldState: { error }}) => {
const options = [
{
label: "Read (default)",
value: "read"
},
{
label: "Write",
value: "write"
}
];
return (
<FormControl
label="Permissions"
errorText={error?.message}
isError={Boolean(error)}
>
<>
{options.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={value[optionValue]}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
isDisabled={optionValue === "read"}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</>
</FormControl>
);
}}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
type="submit"
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeProjectLevelPermission.isOpen}
deleteKey="remove"
title="Do you want to remove this permission from the service account?"
onChange={(isOpen) => handlePopUpToggle("removeProjectLevelPermission", isOpen)}
onDeleteApproved={onRemoveProjectLevelPermission}
/>
if (!serviceAccount) return;
const { latestKey } = await getLatestFileKey({
workspaceId: workspace
});
verifyPrivateKey({
privateKey,
publicKey: serviceAccount.publicKey
});
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey: serviceAccount.publicKey,
privateKey
});
await createServiceAccountProjectLevelPermission.mutateAsync({
serviceAccountId,
workspaceId: workspace,
environment,
read,
write,
encryptedKey: ciphertext,
nonce
});
handlePopUpClose("addProjectLevelPermission");
};
const onRemoveProjectLevelPermission = async () => {
const serviceAccountWorkspacePermissionId = (
popUp?.removeProjectLevelPermission?.data as { _id: string }
)?._id;
await deleteServiceAccountProjectLevelPermission.mutateAsync({
serviceAccountId,
serviceAccountWorkspacePermissionId
});
handlePopUpClose("removeProjectLevelPermission");
};
return (
<div className="w-full bg-white/5 p-6">
<p className="mb-4 text-xl font-semibold">Project-Level Permissions</p>
<div className="mb-4 flex">
<div className="mr-4 flex-1">
<Input
value={searchPermissions}
onChange={(e) => setSearchPermissions(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search service account project-level permissions..."
/>
</div>
);
}
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("addProjectLevelPermission");
reset();
}}
>
Add Permission
</Button>
</div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Project</Th>
<Th>Environment</Th>
<Th>Read</Th>
<Th>Write</Th>
<Th aria-label="actions" />
</Tr>
</THead>
<TBody>
{isPermissionsLoading && (
<TableSkeleton columns={6} innerKey="service-account-project-level-permissions" />
)}
{!isPermissionsLoading &&
serviceAccountWorkspacePermissions &&
serviceAccountWorkspacePermissions.map(
({ _id, workspace, environment, read, write }) => {
const environmentName = workspace.environments.find(
(env) => env.slug === environment
)?.name;
return (
<Tr key={`service-account-project-level-permission-${_id}`} className="w-full">
<Td>{workspace.name}</Td>
<Td>{environmentName}</Td>
<Td>
<Checkbox id="isReadPermissionEnabled" isChecked={read} isDisabled>
{/**/}
</Checkbox>
</Td>
<Td>
<Checkbox id="isWritePermissionEnabled" isChecked={write} isDisabled>
{/**/}
</Checkbox>
</Td>
<Td>
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen("removeProjectLevelPermission", { _id })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Td>
</Tr>
);
}
)}
{!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && (
<Tr>
<Td colSpan={7} className="py-6 text-center text-bunker-400">
<EmptyState title="No permissions found" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
<Modal
isOpen={popUp?.addProjectLevelPermission?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addProjectLevelPermission", isOpen);
}}
>
<ModalContent
title="Add a Project-Level Permission"
subTitle="The service account will be granted scoped access to the specified project and environment"
>
<form onSubmit={handleSubmit(onAddProjectLevelPermission)}>
{!isUserWorkspacesLoading && userWorkspaces && (
<>
<Controller
control={control}
defaultValue=""
name="privateKey"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Service Account Private Key"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="workspace"
defaultValue={String(userWorkspaces?.[0]?._id)}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Project" errorText={error?.message}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
onChange(e);
setSelectedWorkspace(e);
}}
className="border-mine-shaft-500 w-full border"
>
{userWorkspaces && userWorkspaces.length > 0 ? (
userWorkspaces.map((userWorkspace) => {
return (
<SelectItem
value={userWorkspace._id}
key={`project-${userWorkspace._id}`}
>
{userWorkspace.name}
</SelectItem>
);
})
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="environment"
defaultValue={String(userWorkspaces?.[0]?.environments?.[0]?.slug)}
render={({ field: { onChange, ...field } }) => {
const environments =
userWorkspaces?.find(
/* eslint-disable-next-line no-underscore-dangle */
(userWorkspace) => userWorkspace._id === control?._formValues?.workspace
)?.environments ?? [];
return (
<FormControl label="Environment" className="mt-4">
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="border-mine-shaft-500 w-full border"
>
{environments.length > 0 ? (
environments.map((environment) => {
return (
<SelectItem
value={environment.slug}
key={`environment-${environment.slug}`}
>
{environment.name}
</SelectItem>
);
})
) : (
<SelectItem value="none" key="target-app-none">
No environments found
</SelectItem>
)}
</Select>
</FormControl>
);
}}
/>
</>
)}
<Controller
control={control}
name="permissions"
defaultValue={{
read: true,
write: false
}}
render={({ field: { onChange, value }, fieldState: { error } }) => {
const options = [
{
label: "Read (default)",
value: "read"
},
{
label: "Write",
value: "write"
}
];
return (
<FormControl
label="Permissions"
errorText={error?.message}
isError={Boolean(error)}
>
<>
{options.map(({ label, value: optionValue }) => {
return (
<Checkbox
id={value[optionValue]}
key={optionValue}
className="data-[state=checked]:bg-primary"
isChecked={value[optionValue]}
isDisabled={optionValue === "read"}
onCheckedChange={(state) => {
onChange({
...value,
[optionValue]: state
});
}}
>
{label}
</Checkbox>
);
})}
</>
</FormControl>
);
}}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
type="submit"
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeProjectLevelPermission.isOpen}
deleteKey="remove"
title="Do you want to remove this permission from the service account?"
onChange={(isOpen) => handlePopUpToggle("removeProjectLevelPermission", isOpen)}
onDeleteApproved={onRemoveProjectLevelPermission}
/>
</div>
);
};
@@ -1,9 +1,5 @@
import { useState } from "react";
import {
faContactBook,
faMagnifyingGlass,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { faContactBook, faMagnifyingGlass, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -23,13 +19,11 @@ import {
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useDeleteIncidentContact,
useGetOrgIncidentContact} from "@app/hooks/api";
import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api";
export const OrgIncidentContactsTable = () => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { data: contacts, isLoading } = useGetOrgIncidentContact(currentOrg?._id ?? "");
const [searchContact, setSearchContact] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -40,71 +34,71 @@ export const OrgIncidentContactsTable = () => {
const onRemoveIncidentContact = async () => {
try {
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
if (!currentOrg?._id) return;
await mutateAsync({
orgId: currentOrg._id,
email: incidentContactEmail
});
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
createNotification({
text: "Successfully removed incident contact",
type: "success"
});
handlePopUpClose("removeContact");
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"
});
console.error(err);
createNotification({
text: "Failed to remove incident contact",
type: "error"
});
}
};
const filteredContacts = contacts ? contacts.filter(({ email }) =>
email.toLocaleLowerCase().includes(searchContact)
) : [];
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" />
<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} innerKey="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>
</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>
))}
</TBody>
</Table>
{filteredContacts?.length === 0 && !isLoading && (
<EmptyState title="No incident contacts found" icon={faContactBook} />
)}
</TableContainer>
<DeleteActionModal
isOpen={popUp.removeContact.isOpen}
deleteKey="remove"
@@ -1,17 +1,14 @@
import { useEffect, useMemo,useState } from "react";
import {
// Controller,
// useForm
} from "react-hook-form";
import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/router";
import {
faCheck,
faCopy,
faMagnifyingGlass,
faPencil,
faPlus,
faServer,
faTrash} from "@fortawesome/free-solid-svg-icons";
import {
faCheck,
faCopy,
faMagnifyingGlass,
faPencil,
faPlus,
faServer,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { yupResolver } from "@hookform/resolvers/yup";
@@ -21,13 +18,13 @@ import {
Button,
DeleteActionModal,
EmptyState,
// FormControl,
// FormControl,
IconButton,
Input,
Modal,
ModalContent,
// Select,
// SelectItem,
// Select,
// SelectItem,
Table,
TableContainer,
TableSkeleton,
@@ -40,11 +37,15 @@ import {
import { useOrganization, useWorkspace } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
// useCreateServiceAccount,
useDeleteServiceAccount,
useGetServiceAccounts
// useCreateServiceAccount,
useDeleteServiceAccount,
useGetServiceAccounts
} from "@app/hooks/api";
import // Controller,
// useForm
"react-hook-form";
// const serviceAccountExpiration = [
// { label: "1 Day", value: 86400 },
// { label: "7 Days", value: 604800 },
@@ -62,314 +63,312 @@ import {
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
export const OrgServiceAccountsTable = () => {
const router = useRouter();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?._id || "";
const [step, setStep] = useState(0);
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
const [accessKey] = useState("");
const [publicKey] = useState("");
const [privateKey] = useState("");
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addServiceAccount",
"removeServiceAccount",
] as const);
const router = useRouter();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId);
// const createServiceAccount = useCreateServiceAccount();
const removeServiceAccount = useDeleteServiceAccount();
useEffect(() => {
let timer: NodeJS.Timeout;
if (isAccessKeyCopied) {
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
}
const orgId = currentOrg?._id || "";
const [step, setStep] = useState(0);
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
const [accessKey] = useState("");
const [publicKey] = useState("");
const [privateKey] = useState("");
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addServiceAccount",
"removeServiceAccount"
] as const);
if (isPublicKeyCopied) {
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
}
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
useGetServiceAccounts(orgId);
if (isPrivateKeyCopied) {
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
}
// const createServiceAccount = useCreateServiceAccount();
const removeServiceAccount = useDeleteServiceAccount();
return () => clearTimeout(timer);
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
// const {
// control,
// handleSubmit,
// reset,
// formState: { isSubmitting }
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
// if (!currentOrg?._id) return;
// const keyPair = generateKeyPair();
// setPublicKey(keyPair.publicKey);
// setPrivateKey(keyPair.privateKey);
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
// name,
// organizationId: currentOrg?._id,
// publicKey: keyPair.publicKey,
// expiresIn: Number(expiresIn)
// });
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
// setStep(1);
// reset();
// }
const onRemoveServiceAccount = async () => {
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
await removeServiceAccount.mutateAsync(serviceAccountId);
handlePopUpClose("removeServiceAccount");
useEffect(() => {
let timer: NodeJS.Timeout;
if (isAccessKeyCopied) {
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
}
const filteredServiceAccounts = useMemo(
() =>
serviceAccounts.filter(
({ name }) =>
name.toLowerCase().includes(searchServiceAccountFilter)
),
[serviceAccounts, searchServiceAccountFilter]
);
const renderStep = (stepToRender: number) => {
switch (stepToRender) {
case 0:
return (
<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 (
<>
<p>Access Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
<p className="mr-4 break-all">{accessKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(accessKey);
setIsAccessKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isAccessKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
<p className="mt-4">Public Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
<p className="mr-4 break-all">{publicKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(publicKey);
setIsPublicKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isPublicKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
<p className="mt-4">Private Key</p>
<div className="flex items-center justify-end rounded-md p-2 text-base text-gray-400 bg-white/[0.07]">
<p className="mr-4 break-all">{privateKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(privateKey);
setIsPrivateKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isPrivateKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
</>
);
default:
return <div />
}
if (isPublicKeyCopied) {
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
}
return (
<div className="w-full">
<div className="flex justify-between mb-4">
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
setStep(0);
// reset();
handlePopUpOpen("addServiceAccount");
}}
>
Add Service Account
</Button>
</div>
<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>
<Th className="w-full">Valid Until</Th>
<Th aria-label="actions" />
</THead>
<TBody>
{isServiceAccountsLoading && <TableSkeleton columns={5} key="org-service-accounts" />}
{!isServiceAccountsLoading && (
filteredServiceAccounts.map(({
name,
expiresAt,
_id: serviceAccountId
}) => {
return (
<Tr key={`org-service-account-${serviceAccountId}`}>
<Td>{name}</Td>
<Td>{new Date(expiresAt).toUTCString()}</Td>
<Td>
<div className="flex">
<IconButton
ariaLabel="edit"
colorSchema="secondary"
onClick={() => {
if (currentWorkspace?._id) {
router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`);
}
}}
className="mr-2"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</Td>
</Tr>
);
})
)}
</TBody>
</Table>
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
<EmptyState title="No service accounts found" icon={faServer} />
)}
</TableContainer>
<Modal
isOpen={popUp?.addServiceAccount?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addServiceAccount", isOpen);
// reset();
if (isPrivateKeyCopied) {
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
// const {
// control,
// handleSubmit,
// reset,
// formState: { isSubmitting }
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
// if (!currentOrg?._id) return;
// const keyPair = generateKeyPair();
// setPublicKey(keyPair.publicKey);
// setPrivateKey(keyPair.privateKey);
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
// name,
// organizationId: currentOrg?._id,
// publicKey: keyPair.publicKey,
// expiresIn: Number(expiresIn)
// });
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
// setStep(1);
// reset();
// }
const onRemoveServiceAccount = async () => {
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
await removeServiceAccount.mutateAsync(serviceAccountId);
handlePopUpClose("removeServiceAccount");
};
const filteredServiceAccounts = useMemo(
() =>
serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)),
[serviceAccounts, searchServiceAccountFilter]
);
const renderStep = (stepToRender: number) => {
switch (stepToRender) {
case 0:
return (
<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 (
<>
<p>Access Key</p>
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{accessKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(accessKey);
setIsAccessKeyCopied.on();
}}
>
<ModalContent
title="Add Service Account"
subTitle="A service account represents a machine identity such as a VM or application client."
>
{renderStep(step)}
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeServiceAccount.isOpen}
deleteKey="remove"
title="Do you want to remove this service account from the org?"
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
onDeleteApproved={onRemoveServiceAccount}
/>
</div>
);
}
>
<FontAwesomeIcon icon={isAccessKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
<p className="mt-4">Public Key</p>
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{publicKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(publicKey);
setIsPublicKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isPublicKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
<p className="mt-4">Private Key</p>
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{privateKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(privateKey);
setIsPrivateKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isPrivateKeyCopied ? 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">
Copy
</span>
</IconButton>
</div>
</>
);
default:
return <div />;
}
};
return (
<div className="w-full">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
setStep(0);
// reset();
handlePopUpOpen("addServiceAccount");
}}
>
Add Service Account
</Button>
</div>
<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>
<Th className="w-full">Valid Until</Th>
<Th aria-label="actions" />
</THead>
<TBody>
{isServiceAccountsLoading && (
<TableSkeleton columns={5} innerKey="org-service-accounts" />
)}
{!isServiceAccountsLoading &&
filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => {
return (
<Tr key={`org-service-account-${serviceAccountId}`}>
<Td>{name}</Td>
<Td>{new Date(expiresAt).toUTCString()}</Td>
<Td>
<div className="flex">
<IconButton
ariaLabel="edit"
colorSchema="secondary"
onClick={() => {
if (currentWorkspace?._id) {
router.push(
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
);
}
}}
className="mr-2"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() =>
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
<EmptyState title="No service accounts found" icon={faServer} />
)}
</TableContainer>
<Modal
isOpen={popUp?.addServiceAccount?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addServiceAccount", isOpen);
// reset();
}}
>
<ModalContent
title="Add Service Account"
subTitle="A service account represents a machine identity such as a VM or application client."
>
{renderStep(step)}
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeServiceAccount.isOpen}
deleteKey="remove"
title="Do you want to remove this service account from the org?"
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
onDeleteApproved={onRemoveServiceAccount}
/>
</div>
);
};
@@ -1,4 +1,4 @@
import { faKey,faXmark } from "@fortawesome/free-solid-svg-icons";
import { faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -14,99 +14,91 @@ import {
THead,
Tr
} from "@app/components/v2";
import {
useDeleteAPIKey,
useGetMyAPIKeys} from "@app/hooks/api";
import { useDeleteAPIKey, useGetMyAPIKeys } from "@app/hooks/api";
export const APIKeyTable = () => {
const { createNotification } = useNotificationContext();
const { data, isLoading } = useGetMyAPIKeys();
const { mutateAsync } = useDeleteAPIKey();
const { createNotification } = useNotificationContext();
const { data, isLoading } = useGetMyAPIKeys();
const { mutateAsync } = useDeleteAPIKey();
const handleDeleteAPIKeyDataClick = async (apiKeyDataId: string) => {
try {
await mutateAsync(apiKeyDataId);
createNotification({
text: "Successfully deleted API key",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete API key",
type: "error"
});
}
const handleDeleteAPIKeyDataClick = async (apiKeyDataId: string) => {
try {
await mutateAsync(apiKeyDataId);
createNotification({
text: "Successfully deleted API key",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete API key",
type: "error"
});
}
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const formattedDate = `${day}/${month}/${year}`;
return formattedDate;
}
return (
<div>
<TableContainer className="">
<Table>
<THead>
<Tr>
<Th className="flex-1">Name</Th>
<Th className="flex-1">Last active</Th>
<Th className="flex-1">Created</Th>
<Th className="flex-1">Expiration</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} key="api-keys" />}
{!isLoading && data && data.length > 0 && data.map(({
_id,
name,
createdAt,
expiresAt,
lastUsed
}) => {
return (
<Tr className="h-10" key={`api-key-${_id}`}>
<Td>{name}</Td>
<Td>{formatDate(lastUsed)}</Td>
<Td>{formatDate(createdAt)}</Td>
<Td>{formatDate(expiresAt)}</Td>
<Td>
<IconButton
onClick={async () => {
await handleDeleteAPIKeyDataClick(_id);
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No API Keys on file"
icon={faKey}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
);
}
};
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const formattedDate = `${day}/${month}/${year}`;
return formattedDate;
};
return (
<div>
<TableContainer className="">
<Table>
<THead>
<Tr>
<Th className="flex-1">Name</Th>
<Th className="flex-1">Last active</Th>
<Th className="flex-1">Created</Th>
<Th className="flex-1">Expiration</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="api-keys" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => {
return (
<Tr className="h-10" key={`api-key-${_id}`}>
<Td>{name}</Td>
<Td>{formatDate(lastUsed)}</Td>
<Td>{formatDate(createdAt)}</Td>
<Td>{formatDate(expiresAt)}</Td>
<Td>
<IconButton
onClick={async () => {
await handleDeleteAPIKeyDataClick(_id);
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No API Keys on file" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
);
};
@@ -14,61 +14,54 @@ import {
import { useGetMySessions } from "@app/hooks/api";
export const SessionsTable = () => {
const { data, isLoading } = useGetMySessions();
const { data, isLoading } = useGetMySessions();
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const formattedDate = `${day}/${month}/${year}`;
return formattedDate;
}
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Created</Th>
<Th>Last active</Th>
<Th>IP address</Th>
<Th>Device</Th>
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} key="sesssions" />}
{!isLoading && data && data.length > 0 && data.map(({
_id,
createdAt,
lastUsed,
ip,
userAgent
}) => {
return (
<Tr className="h-10" key={`session-${_id}`}>
<Td>{formatDate(createdAt)}</Td>
<Td>{formatDate(lastUsed)}</Td>
<Td>{ip}</Td>
<Td>{userAgent}</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState
title="No sessions on file"
icon={faServer}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const formattedDate = `${day}/${month}/${year}`;
return formattedDate;
};
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Created</Th>
<Th>Last active</Th>
<Th>IP address</Th>
<Th>Device</Th>
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="sesssions" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({ _id, createdAt, lastUsed, ip, userAgent }) => {
return (
<Tr className="h-10" key={`session-${_id}`}>
<Td>{formatDate(createdAt)}</Td>
<Td>{formatDate(lastUsed)}</Td>
<Td>{ip}</Td>
<Td>{userAgent}</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No sessions on file" icon={faServer} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -11,79 +11,79 @@ import {
Td,
Th,
THead,
Tr,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "upgradePlan"]>,
{
name,
slug
}: {
name: string;
slug: string;
}
) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["updateEnv", "deleteEnv", "upgradePlan"]>,
{
name,
slug
}: {
name: string;
slug: string;
}
) => void;
};
export const EnvironmentTable = ({
handlePopUpOpen
}: Props) => {
const { currentWorkspace, isLoading } = useWorkspace();
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th aria-label="button" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={3} key="project-envs" />}
{!isLoading && currentWorkspace && currentWorkspace.environments.map(({ name, slug }) => (
<Tr key={name}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td className="flex items-center justify-end">
<IconButton
className="mr-3 py-2"
onClick={() => {
handlePopUpOpen("updateEnv", { name, slug });
}}
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
onClick={() => {
handlePopUpOpen("deleteEnv", { name, slug });
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
))}
{!isLoading && currentWorkspace && currentWorkspace.environments?.length === 0 && (
<Tr>
<Td colSpan={3}>
<EmptyState title="No environments found" />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace, isLoading } = useWorkspace();
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th aria-label="button" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={3} innerKey="project-envs" />}
{!isLoading &&
currentWorkspace &&
currentWorkspace.environments.map(({ name, slug }) => (
<Tr key={name}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td className="flex items-center justify-end">
<IconButton
className="mr-3 py-2"
onClick={() => {
handlePopUpOpen("updateEnv", { name, slug });
}}
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
onClick={() => {
handlePopUpOpen("deleteEnv", { name, slug });
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
))}
{!isLoading && currentWorkspace && currentWorkspace.environments?.length === 0 && (
<Tr>
<Td colSpan={3}>
<EmptyState title="No environments found" />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -18,65 +18,65 @@ import { useGetWsTags } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteTagConfirmation"]>,
{
name,
id
}: {
name: string;
id: string;
}
) => void;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteTagConfirmation"]>,
{
name,
id
}: {
name: string;
id: string;
}
) => void;
};
export const SecretTagsTable = ({
handlePopUpOpen
}: Props) => {
const { currentWorkspace }= useWorkspace();
const { data, isLoading } = useGetWsTags(currentWorkspace?._id ?? "");
export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetWsTags(currentWorkspace?._id ?? "");
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Tag</Th>
<Th>Slug</Th>
<Th aria-label="button" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={3} key="secret-tags" />}
{!isLoading && data && data.map(({ _id, name, slug }) => (
<Tr key={name}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td className="flex items-center justify-end">
<IconButton
onClick={() =>
handlePopUpOpen("deleteTagConfirmation", {
name,
id: _id
})
}
colorSchema="danger"
ariaLabel="update"
>
<FontAwesomeIcon icon={faTrashCan} />
</IconButton>
</Td>
</Tr>
))}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={3}>
<EmptyState title="No secret tags found" icon={faTags} />
return (
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Tag</Th>
<Th>Slug</Th>
<Th aria-label="button" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={3} innerKey="secret-tags" />}
{!isLoading &&
data &&
data.map(({ _id, name, slug }) => (
<Tr key={name}>
<Td>{name}</Td>
<Td>{slug}</Td>
<Td className="flex items-center justify-end">
<IconButton
onClick={() =>
handlePopUpOpen("deleteTagConfirmation", {
name,
id: _id
})
}
colorSchema="danger"
ariaLabel="update"
>
<FontAwesomeIcon icon={faTrashCan} />
</IconButton>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
))}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={3}>
<EmptyState title="No secret tags found" icon={faTags} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -48,7 +48,7 @@ export const ServiceTokenTable = ({ handlePopUpOpen }: Props) => {
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} key="project-service-tokens" />}
{isLoading && <TableSkeleton columns={4} innerKey="project-service-tokens" />}
{!isLoading &&
data &&
data.map((row) => (
@@ -160,7 +160,7 @@ export const WebhooksTab = () => {
</Tr>
</THead>
<TBody>
{isWebhooksLoading && <TableSkeleton columns={5} key="webhooks-loading" />}
{isWebhooksLoading && <TableSkeleton columns={5} innerKey="webhooks-loading" />}
{!isWebhooksLoading && webhooks && webhooks?.length === 0 && (
<Tr>
<Td colSpan={5}>