mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 01:05:40 +00:00
feat: added new secret input component and updated toolbar key special prop to innerKey
This commit is contained in:
+3
-3
@@ -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>
|
||||
|
||||
+120
-85
@@ -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't accepted the invite yet</Tag>
|
||||
: <Tag colorSchema="red">This user isn'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't accepted the invite yet
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag colorSchema="red">
|
||||
This user isn'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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
+313
-313
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+55
-70
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+117
-154
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+61
-71
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+109
-116
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+61
-70
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+386
-381
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+60
-66
@@ -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"
|
||||
|
||||
+322
-323
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+50
-57
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+69
-69
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+57
-57
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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) => (
|
||||
|
||||
+1
-1
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user