feat(infisical-pg): changed frontend api types _id to normal id

This commit is contained in:
Akhil Mohan
2024-01-27 12:38:22 +05:30
parent f3b37de3f3
commit aaba4a0895
161 changed files with 5358 additions and 4510 deletions
@@ -162,7 +162,8 @@ export const IntegrationsSection = ({
</div>
</div>
)}
{((integration.integration === "checkly") || (integration.integration === "github")) && (
{(integration.integration === "checkly" ||
integration.integration === "github") && (
<>
{integration.targetService && (
<div className="ml-2">
+17 -17
View File
@@ -3,22 +3,22 @@ import { NextRouter } from "next/router";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => {
const userOrgs = await fetchOrganizations();
const userOrgs = await fetchOrganizations();
if (organizationId) {
localStorage.setItem("orgData.id", organizationId);
router.push(`/org/${organizationId}/overview`);
return;
}
if (organizationId) {
localStorage.setItem("orgData.id", organizationId);
router.push(`/org/${organizationId}/overview`);
return;
}
if (userOrgs.length > 0) {
// user is part of at least 1 org
const userOrg = userOrgs[0] && userOrgs[0].id;
localStorage.setItem("orgData.id", userOrg);
router.push(`/org/${userOrg}/overview`);
} else {
// user is not part of any org
localStorage.removeItem("orgData.id");
router.push("/org/none");
}
}
if (userOrgs.length > 0) {
// user is part of at least 1 org
const userOrg = userOrgs[0] && userOrgs[0].id;
localStorage.setItem("orgData.id", userOrg);
router.push(`/org/${userOrg}/overview`);
} else {
// user is not part of any org
localStorage.removeItem("orgData.id");
router.push("/org/none");
}
};
@@ -3,11 +3,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import {
OrgIdentityTab,
OrgMembersTab,
OrgRoleTabSection
} from "./components";
import { OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components";
enum TabSections {
Member = "members",
@@ -19,17 +15,15 @@ export const MembersPage = withPermission(
() => {
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">
Organization Access Control
</p>
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<p className="mr-4 mb-4 text-3xl font-semibold text-white">Organization Access Control</p>
<Tabs defaultValue={TabSections.Member}>
<TabList>
<Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.Identities}>
<div className="flex items-center">
<p>Machine Identities</p>
<div className="ml-2 rounded-md text-yellow text-sm inline-block bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] opacity-80 hover:opacity-100 cursor-default">
<div className="ml-2 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
New
</div>
</div>
@@ -0,0 +1,577 @@
import { useCallback, 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import {
Button,
DeleteActionModal,
EmailServiceSetupModal,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Select,
SelectItem,
Table,
TableContainer,
TableSkeleton,
Tag,
TBody,
Td,
Th,
THead,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription,
useUser,
useWorkspace
} from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import {
useAddUserToOrg,
useDeleteOrgMembership,
useGetOrgUsers,
useGetSSOConfig,
useGetUserWorkspaceMemberships,
useGetUserWsKey,
useUpdateOrgUserRole,
useUploadWsKey
} from "@app/hooks/api";
import { TRole } from "@app/hooks/api/roles/types";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
type Props = {
roles?: TRole<undefined>[];
isRolesLoading?: boolean;
};
const addMemberFormSchema = yup.object({
email: yup.string().email().required().label("Email").trim().lowercase()
});
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
export const OrgMembersTable = ({ roles = [], isRolesLoading }: Props) => {
const router = useRouter();
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const { workspaces, currentWorkspace } = useWorkspace();
const { user } = useUser();
const userId = user?.id || "";
const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || "";
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const { data: serverDetails } = useFetchServerStatus();
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addMember",
"removeMember",
"upgradePlan",
"setUpEmail"
] as const);
const { subscription } = useSubscription();
const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId);
const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
useGetUserWorkspaceMemberships(orgId);
const { data: wsKey } = useGetUserWsKey(workspaceId);
const removeUserOrgMembership = useDeleteOrgMembership();
const addUserToOrg = useAddUserToOrg();
const updateOrgUserRole = useUpdateOrgUserRole();
const uploadWsKey = useUploadWsKey();
const [completeInviteLink, setCompleteInviteLink] = useState<string | undefined>("");
const isMoreUsersNotAllowed = subscription?.memberLimit
? subscription.membersUsed >= subscription.memberLimit
: false;
useEffect(() => {
if (router.query.action === "invite") {
handlePopUpOpen("addMember");
}
}, []);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
const onAddMember = async ({ email }: TAddMemberForm) => {
if (!currentOrg?.id) return;
try {
const { data } = await addUserToOrg.mutateAsync({
organizationId: currentOrg?.id,
inviteeEmail: email
});
setCompleteInviteLink(data?.completeInviteLink);
// only show this notification when email is configured.
// A [completeInviteLink] will not be sent if smtp is configured
if (!data.completeInviteLink) {
createNotification({
text: "Successfully invited user to the organization.",
type: "success"
});
}
} catch (error) {
console.error(error);
createNotification({
text: "Failed to invite user to org",
type: "error"
});
}
if (serverDetails?.emailConfigured) {
handlePopUpClose("addMember");
}
reset();
};
const onAddUserToOrg = async (email: string) => {
if (!currentOrg?.id) return;
try {
const { data } = await addUserToOrg.mutateAsync({
organizationId: currentOrg?.id,
inviteeEmail: email
});
setCompleteInviteLink(data?.completeInviteLink);
// only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured
if (!data.completeInviteLink) {
createNotification({
text: "Successfully invited user to the organization.",
type: "success"
});
}
} catch (error) {
console.error(error);
createNotification({
text: "Failed to invite user to org",
type: "error"
});
}
};
const onRemoveOrgMemberApproved = async () => {
const membershipId = (popUp?.removeMember?.data as { id: string })?.id;
if (!currentOrg?.id) return;
try {
await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?.id, membershipId });
createNotification({
text: "Successfully removed user from org",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to remove user from the organization",
type: "error"
});
}
handlePopUpClose("removeMember");
};
const isIamOwner = useMemo(
() => members?.find(({ user: u }) => userId === u?.id)?.role === "owner",
[userId, members]
);
const findRoleFromId = useCallback(
(roleId: string) => {
return roles.find(({ id }) => id === roleId);
},
[roles]
);
const filterdUser = useMemo(
() =>
members?.filter(
({ user: u, inviteEmail }) =>
u?.firstName?.toLowerCase().includes(searchMemberFilter) ||
u?.lastName?.toLowerCase().includes(searchMemberFilter) ||
u?.email?.toLowerCase().includes(searchMemberFilter) ||
inviteEmail?.includes(searchMemberFilter)
),
[members, searchMemberFilter]
);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isInviteLinkCopied) {
timer = setTimeout(() => setInviteLinkCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isInviteLinkCopied]);
const onRoleChange = async (membershipId: string, role: string) => {
if (!currentOrg?.id) return;
try {
await updateOrgUserRole.mutateAsync({ organizationId: currentOrg?.id, membershipId, role });
createNotification({
text: "Successfully updated user role",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to update user role",
type: "error"
});
}
};
const onGrantAccess = async (grantedUserId: string, publicKey: string) => {
try {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
if (!PRIVATE_KEY || !wsKey) return;
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey,
privateKey: PRIVATE_KEY
});
await uploadWsKey.mutateAsync({
userId: grantedUserId,
nonce,
encryptedKey: ciphertext,
workspaceId: currentWorkspace?.id || ""
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to grant access to user",
type: "error"
});
}
};
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(completeInviteLink as string);
setInviteLinkCopied.on();
};
const isLoading = isMembersLoading || IsWsMembershipLoading || isRolesLoading;
return (
<div className="w-full">
<div className="mb-4 flex">
<div className="mr-4 flex-1">
<Input
value={searchMemberFilter}
onChange={(e) => setSearchMemberFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
createNotification({
text: "You cannot invite users when SAML SSO is configured for your organization",
type: "error"
});
return;
}
if (isMoreUsersNotAllowed) {
handlePopUpOpen("upgradePlan");
} else {
handlePopUpOpen("addMember");
}
}}
>
Add Member
</Button>
)}
</OrgPermissionCan>
</div>
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Email</Th>
<Th>Role</Th>
<Th>Projects</Th>
<Th aria-label="actions" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
{!isLoading &&
filterdUser?.map(
({ user: u, inviteEmail, role, customRole, id: orgMembershipId, status }) => {
const name = u ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail;
const userWs = workspaceMemberships?.[u?.id];
return (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<>
{status === "accepted" && (
<Select
defaultValue={
role === "custom" ? findRoleFromId(customRole)?.slug : role
}
isDisabled={userId === u?.id || !isAllowed}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
onRoleChange(orgMembershipId, selectedRole)
}
>
{roles
.filter(({ slug }) =>
slug === "owner" ? isIamOwner || role === "owner" : true
)
.map(({ slug, name: roleName }) => (
<SelectItem value={slug} key={`owner-option-${slug}`}>
{roleName}
</SelectItem>
))}
</Select>
)}
{(status === "invited" || status === "verified") &&
serverDetails?.emailConfigured && (
<Button
isDisabled={!isAllowed}
className="w-40"
colorSchema="primary"
variant="outline_bg"
onClick={() => onAddUserToOrg(email)}
>
Resend Invite
</Button>
)}
{status === "completed" && (
<Button
colorSchema="secondary"
isDisabled={!isAllowed}
onClick={() => onGrantAccess(u?.id, u?.publicKey)}
>
Grant Access
</Button>
)}
</>
)}
</OrgPermissionCan>
</Td>
<Td>
{userWs ? (
userWs?.map(({ name: wsName, id }) => (
<Tag key={`user-${u.id}-workspace-${id}`} className="my-1">
{wsName}
</Tag>
))
) : (
<div className="flex flex-row">
{(status === "invited" || status === "verified") &&
serverDetails?.emailConfigured ? (
<Tag colorSchema="red">
This user hasn&apos;t accepted the invite yet
</Tag>
) : (
<Tag colorSchema="red">
This user isn&apos;t part of any projects yet
</Tag>
)}
{router.query.id !== "undefined" &&
!(
(status === "invited" || status === "verified") &&
serverDetails?.emailConfigured
) && (
<button
type="button"
onClick={() =>
router.push(`/project/${workspaces[0]?.id}/members`)
}
className="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>
<Td>
{userId !== u?.id && (
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<IconButton
ariaLabel="delete"
colorSchema="danger"
isDisabled={userId === u?.id || !isAllowed}
onClick={() =>
handlePopUpOpen("removeMember", { id: orgMembershipId })
}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
)}
</Td>
</Tr>
);
}
)}
</TBody>
</Table>
{!isLoading && filterdUser?.length === 0 && (
<EmptyState title="No project members found" icon={faUsers} />
)}
</TableContainer>
</div>
<Modal
isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addMember", isOpen);
setCompleteInviteLink(undefined);
}}
>
<ModalContent
title={`Invite others to ${currentOrg?.name}`}
subTitle={
<div>
{!completeInviteLink && (
<div>
An invite is specific to an email address and expires after 1 day.
<br />
For security reasons, you will need to separately add members to projects.
</div>
)}
{completeInviteLink &&
"This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
</div>
}
>
{!completeInviteLink && (
<form onSubmit={handleSubmit(onAddMember)}>
<Controller
control={control}
defaultValue=""
name="email"
render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add Member
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addMember")}
>
Cancel
</Button>
</div>
</form>
)}
{completeInviteLink && (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{completeInviteLink}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
click to copy
</span>
</IconButton>
</div>
)}
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
deleteKey="remove"
title="Do you want to remove this user from the org?"
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
onDeleteApproved={onRemoveOrgMemberApproved}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan."
/>
<EmailServiceSetupModal
isOpen={popUp.setUpEmail?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
/>
</div>
);
};
@@ -89,7 +89,7 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const orgId = currentOrg?._id || "";
const orgId = currentOrg?.id || "";
const {
handleSubmit,
register,
@@ -105,12 +105,12 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
const { mutateAsync: updateRole } = useUpdateRole();
const handleRoleUpdate = async (el: TFormSchema) => {
if (!role?._id) return;
if (!role?.id) return;
try {
await updateRole({
orgId,
id: role?._id,
id: role?.id,
...el,
permissions: formRolePermission2API(el.permissions)
});
@@ -145,7 +145,7 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
return (
<div>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="flex justify-between mb-2 items-center">
<div className="mb-2 flex items-center justify-between">
<h1 className="text-xl font-semibold text-mineshaft-100">
{isNewRole ? "New" : "Edit"} Role
</h1>
@@ -187,7 +187,7 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
>
<Input {...register("description")} isReadOnly={isNonEditable} />
</FormControl>
<div className="flex justify-between items-center pt-6 border-t border-t-mineshaft-800">
<div className="flex items-center justify-between border-t border-t-mineshaft-800 pt-6">
<div>
<h2 className="text-xl font-medium">Add Permission</h2>
</div>
@@ -213,7 +213,7 @@ export const OrgRoleModifySection = ({ role, onGoBack }: Props) => {
</div>
))}
</div>
<div className="flex items-center space-x-4 mt-12">
<div className="mt-12 flex items-center space-x-4">
<Button
type="submit"
isDisabled={isSubmitting || isNonEditable || !isDirty}
@@ -27,12 +27,10 @@ type Props = {
onSelectRole: (role?: TRole<undefined>) => void;
};
export const OrgRoleTable = ({
onSelectRole
}: Props) => {
export const OrgRoleTable = ({ onSelectRole }: Props) => {
const [searchRoles, setSearchRoles] = useState("");
const { currentOrg } = useOrganization();
const orgId = currentOrg?._id || "";
const orgId = currentOrg?.id || "";
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
@@ -43,7 +41,7 @@ export const OrgRoleTable = ({
const { mutateAsync: deleteRole } = useDeleteRole();
const handleRoleDelete = async () => {
const { _id: id } = popUp?.deleteRole?.data as TRole<undefined>;
const { id } = popUp?.deleteRole?.data as TRole<undefined>;
try {
await deleteRole({
orgId,
@@ -93,7 +91,7 @@ export const OrgRoleTable = ({
<TBody>
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{(roles as TRole<undefined>[])?.map((role) => {
const { _id: id, name, slug } = role;
const { id, name, slug } = role;
const isNonMutatable = ["owner", "admin", "member", "no-access"].includes(slug);
return (
@@ -50,10 +50,10 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
type: "success"
});
if (router.isReady) router.push(`/org/${organization._id}/overview`);
else window.location.href = `/org/${organization._id}/overview`;
if (router.isReady) router.push(`/org/${organization.id}/overview`);
else window.location.href = `/org/${organization.id}/overview`;
localStorage.setItem("orgData.id", organization._id);
localStorage.setItem("orgData.id", organization.id);
reset();
onClose();
@@ -28,7 +28,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?._id ?? "");
const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?.id ?? "");
const renderActorSelectItem = (actor: Actor) => {
switch (actor.type) {
@@ -69,7 +69,7 @@ export const LogsFilter = ({ control, reset }: Props) => {
};
return (
<div className="flex justify-between items-center sticky top-20 z-10 bg-bunker-800">
<div className="sticky top-20 z-10 flex items-center justify-between bg-bunker-800">
<div className="flex items-center">
<Controller
control={control}
@@ -79,13 +79,13 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Event"
errorText={error?.message}
isError={Boolean(error)}
className="w-40 mr-4"
className="mr-4 w-40"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100"
className="w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100"
>
{eventTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
@@ -105,13 +105,13 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Actor"
errorText={error?.message}
isError={Boolean(error)}
className="w-40 mr-4"
className="mr-4 w-40"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100"
className="w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100"
>
{data.map((actor) => renderActorSelectItem(actor))}
</Select>
@@ -127,13 +127,13 @@ export const LogsFilter = ({ control, reset }: Props) => {
label="Source"
errorText={error?.message}
isError={Boolean(error)}
className="w-40 mr-4"
className="mr-4 w-40"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100"
className="w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100"
>
{userAgentTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
@@ -33,7 +33,7 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
const { currentWorkspace } = useWorkspace();
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
currentWorkspace?._id ?? "",
currentWorkspace?.id ?? "",
{
eventType,
userAgentType,
@@ -64,7 +64,7 @@ export const LogsTable = ({ eventType, userAgentType, actor, startDate, endDate
data?.pages?.map((group, i) => (
<Fragment key={`auditlog-item-${i + 1}`}>
{group.map((auditLog) => (
<LogsTableRow auditLog={auditLog} key={`audit-log-${auditLog._id}`} />
<LogsTableRow auditLog={auditLog} key={`audit-log-${auditLog.id}`} />
))}
</Fragment>
))}
@@ -1,367 +1,358 @@
import {
Td,
Tr
} from "@app/components/v2";
import { Td, Tr } from "@app/components/v2";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
type Props = {
auditLog: AuditLog
}
auditLog: AuditLog;
};
export const LogsTableRow = ({
auditLog
}: Props) => {
const renderActor = (actor: Actor) => {
switch (actor.type) {
case ActorType.USER:
return (
<Td>
<p>{actor.metadata.email}</p>
<p>User</p>
</Td>
);
case ActorType.SERVICE:
return (
<Td>
<p>{`${actor.metadata.name}`}</p>
<p>Service token</p>
</Td>
);
case ActorType.IDENTITY:
return (
<Td>
<p>{`${actor.metadata.name}`}</p>
<p>Machine Identity</p>
</Td>
);
default:
return (
<Td />
);
}
export const LogsTableRow = ({ auditLog }: Props) => {
const renderActor = (actor: Actor) => {
switch (actor.type) {
case ActorType.USER:
return (
<Td>
<p>{actor.metadata.email}</p>
<p>User</p>
</Td>
);
case ActorType.SERVICE:
return (
<Td>
<p>{`${actor.metadata.name}`}</p>
<p>Service token</p>
</Td>
);
case ActorType.IDENTITY:
return (
<Td>
<p>{`${actor.metadata.name}`}</p>
<p>Machine Identity</p>
</Td>
);
default:
return <Td />;
}
const renderMetadata = (event: Event) => {
switch (event.type) {
case EventType.GET_SECRETS:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`# Secrets: ${event.metadata.numberOfSecrets}`}</p>
</Td>
);
case EventType.GET_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.CREATE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.UPDATE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.DELETE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.AUTHORIZE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
</Td>
);
case EventType.UNAUTHORIZE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
</Td>
);
case EventType.CREATE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
{event.metadata.app && (
<p>{`Target app: ${event.metadata.app}`}</p>
)}
{event.metadata.appId && (
<p>{`Target app: ${event.metadata.appId}`}</p>
)}
{event.metadata.targetEnvironment && (
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
)}
{event.metadata.targetEnvironmentId && (
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
)}
</Td>
);
case EventType.DELETE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
{event.metadata.app && (
<p>{`Target App: ${event.metadata.app}`}</p>
)}
{event.metadata.appId && (
<p>{`Target app: ${event.metadata.appId}`}</p>
)}
{event.metadata.targetEnvironment && (
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
)}
{event.metadata.targetEnvironmentId && (
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
)}
</Td>
);
case EventType.ADD_TRUSTED_IP:
return (
<Td>
<p>{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}</p>
</Td>
);
case EventType.UPDATE_TRUSTED_IP:
return (
<Td>
<p>{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}</p>
</Td>
);
case EventType.DELETE_TRUSTED_IP:
return (
<Td>
<p>{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}</p>
</Td>
);
case EventType.CREATE_SERVICE_TOKEN:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.DELETE_SERVICE_TOKEN:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.CREATE_IDENTITY:
return (
<Td>
<p>{`ID: ${event.metadata.identityId}`}</p>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.UPDATE_IDENTITY:
return (
<Td>
<p>{`ID: ${event.metadata.identityId}`}</p>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.DELETE_IDENTITY:
return (
<Td>
<p>{`ID: ${event.metadata.identityId}`}</p>
</Td>
);
case EventType.CREATE_ENVIRONMENT:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
<p>{`Slug: ${event.metadata.slug}`}</p>
</Td>
);
case EventType.UPDATE_ENVIRONMENT:
return (
<Td>
<p>{`Old name: ${event.metadata.oldName}`}</p>
<p>{`New name: ${event.metadata.newName}`}</p>
<p>{`Old slug: ${event.metadata.oldSlug}`}</p>
<p>{`New slug: ${event.metadata.newSlug}`}</p>
</Td>
);
case EventType.DELETE_ENVIRONMENT:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
<p>{`Slug: ${event.metadata.slug}`}</p>
</Td>
);
case EventType.ADD_WORKSPACE_MEMBER:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
</Td>
);
case EventType.REMOVE_WORKSPACE_MEMBER:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
</Td>
);
case EventType.CREATE_FOLDER:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.folderPath}`}</p>
<p>{`Folder: ${event.metadata.folderName}`}</p>
</Td>
);
case EventType.UPDATE_FOLDER:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.folderPath}`}</p>
<p>{`Old folder: ${event.metadata.oldFolderName}`}</p>
<p>{`New folder: ${event.metadata.newFolderName}`}</p>
</Td>
);
case EventType.DELETE_FOLDER:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.folderPath}`}</p>
<p>{`Folder: ${event.metadata.folderName}`}</p>
</Td>
);
case EventType.CREATE_WEBHOOK:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
case EventType.UPDATE_WEBHOOK_STATUS:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
case EventType.DELETE_WEBHOOK:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
case EventType.GET_SECRET_IMPORTS:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`# Imported paths: ${event.metadata.numberOfImports}`}</p>
</Td>
);
case EventType.CREATE_SECRET_IMPORT:
return (
<Td>
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
case EventType.UPDATE_SECRET_IMPORT:
return (
<Td>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
case EventType.DELETE_SECRET_IMPORT:
return (
<Td>
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
case EventType.UPDATE_USER_WORKSPACE_ROLE:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
<p>{`Old role: ${event.metadata.oldRole}`}</p>
<p>{`New role: ${event.metadata.newRole}`}</p>
</Td>
);
case EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
{event.metadata.deniedPermissions.map((permission) => {
return (
<p key={`audit-log-denied-permission-${event.metadata.userId}-${permission.environmentSlug}-${permission.ability}`}>
{`Denied env-ability: ${permission.environmentSlug}-${permission.ability}`}
</p>
);
})}
</Td>
);
default:
return (
<Td />
);
}
};
const renderMetadata = (event: Event) => {
switch (event.type) {
case EventType.GET_SECRETS:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`# Secrets: ${event.metadata.numberOfSecrets}`}</p>
</Td>
);
case EventType.GET_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.CREATE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.UPDATE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.DELETE_SECRET:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
<p>{`Secret: ${event.metadata.secretKey}`}</p>
</Td>
);
case EventType.AUTHORIZE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
</Td>
);
case EventType.UNAUTHORIZE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
</Td>
);
case EventType.CREATE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
{event.metadata.app && <p>{`Target app: ${event.metadata.app}`}</p>}
{event.metadata.appId && <p>{`Target app: ${event.metadata.appId}`}</p>}
{event.metadata.targetEnvironment && (
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
)}
{event.metadata.targetEnvironmentId && (
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
)}
</Td>
);
case EventType.DELETE_INTEGRATION:
return (
<Td>
<p>{`Integration: ${event.metadata.integration}`}</p>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.secretPath}`}</p>
{event.metadata.app && <p>{`Target App: ${event.metadata.app}`}</p>}
{event.metadata.appId && <p>{`Target app: ${event.metadata.appId}`}</p>}
{event.metadata.targetEnvironment && (
<p>{`Target environment: ${event.metadata.targetEnvironment}`}</p>
)}
{event.metadata.targetEnvironmentId && (
<p>{`Target environment ID: ${event.metadata.targetEnvironmentId}`}</p>
)}
</Td>
);
case EventType.ADD_TRUSTED_IP:
return (
<Td>
<p>{`IP: ${event.metadata.ipAddress}${
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
}`}</p>
</Td>
);
case EventType.UPDATE_TRUSTED_IP:
return (
<Td>
<p>{`IP: ${event.metadata.ipAddress}${
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
}`}</p>
</Td>
);
case EventType.DELETE_TRUSTED_IP:
return (
<Td>
<p>{`IP: ${event.metadata.ipAddress}${
event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""
}`}</p>
</Td>
);
case EventType.CREATE_SERVICE_TOKEN:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.DELETE_SERVICE_TOKEN:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.CREATE_IDENTITY:
return (
<Td>
<p>{`ID: ${event.metadata.identityId}`}</p>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.UPDATE_IDENTITY:
return (
<Td>
<p>{`ID: ${event.metadata.identityId}`}</p>
<p>{`Name: ${event.metadata.name}`}</p>
</Td>
);
case EventType.DELETE_IDENTITY:
return (
<Td>
<p>{`ID: ${event.metadata.identityId}`}</p>
</Td>
);
case EventType.CREATE_ENVIRONMENT:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
<p>{`Slug: ${event.metadata.slug}`}</p>
</Td>
);
case EventType.UPDATE_ENVIRONMENT:
return (
<Td>
<p>{`Old name: ${event.metadata.oldName}`}</p>
<p>{`New name: ${event.metadata.newName}`}</p>
<p>{`Old slug: ${event.metadata.oldSlug}`}</p>
<p>{`New slug: ${event.metadata.newSlug}`}</p>
</Td>
);
case EventType.DELETE_ENVIRONMENT:
return (
<Td>
<p>{`Name: ${event.metadata.name}`}</p>
<p>{`Slug: ${event.metadata.slug}`}</p>
</Td>
);
case EventType.ADD_WORKSPACE_MEMBER:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
</Td>
);
case EventType.REMOVE_WORKSPACE_MEMBER:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
</Td>
);
case EventType.CREATE_FOLDER:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.folderPath}`}</p>
<p>{`Folder: ${event.metadata.folderName}`}</p>
</Td>
);
case EventType.UPDATE_FOLDER:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.folderPath}`}</p>
<p>{`Old folder: ${event.metadata.oldFolderName}`}</p>
<p>{`New folder: ${event.metadata.newFolderName}`}</p>
</Td>
);
case EventType.DELETE_FOLDER:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Path: ${event.metadata.folderPath}`}</p>
<p>{`Folder: ${event.metadata.folderName}`}</p>
</Td>
);
case EventType.CREATE_WEBHOOK:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
case EventType.UPDATE_WEBHOOK_STATUS:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
case EventType.DELETE_WEBHOOK:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
case EventType.GET_SECRET_IMPORTS:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`# Imported paths: ${event.metadata.numberOfImports}`}</p>
</Td>
);
case EventType.CREATE_SECRET_IMPORT:
return (
<Td>
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
case EventType.UPDATE_SECRET_IMPORT:
return (
<Td>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
case EventType.DELETE_SECRET_IMPORT:
return (
<Td>
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
case EventType.UPDATE_USER_WORKSPACE_ROLE:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
<p>{`Old role: ${event.metadata.oldRole}`}</p>
<p>{`New role: ${event.metadata.newRole}`}</p>
</Td>
);
case EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
{event.metadata.deniedPermissions.map((permission) => {
return (
<p
key={`audit-log-denied-permission-${event.metadata.userId}-${permission.environmentSlug}-${permission.ability}`}
>
{`Denied env-ability: ${permission.environmentSlug}-${permission.ability}`}
</p>
);
})}
</Td>
);
default:
return <Td />;
}
};
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const formatDate = (dateToFormat: string) => {
const date = new Date(dateToFormat);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
let hours = date.getHours();
const minutes = String(date.getMinutes()).padStart(2, "0");
let hours = date.getHours();
const minutes = String(date.getMinutes()).padStart(2, "0");
// convert from 24h to 12h format
const period = hours >= 12 ? "PM" : "AM";
hours %= 12;
hours = hours || 12; // the hour '0' should be '12'
// convert from 24h to 12h format
const period = hours >= 12 ? "PM" : "AM";
hours %= 12;
hours = hours || 12; // the hour '0' should be '12'
const formattedDate = `${day}-${month}-${year} at ${hours}:${minutes} ${period}`;
return formattedDate;
}
return (
<Tr className={`log-${auditLog._id} h-10 border-b border-x-0 border-t-0`}>
<Td>{formatDate(auditLog.createdAt)}</Td>
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
{renderActor(auditLog.actor)}
<Td>
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
<p>{auditLog.ipAddress}</p>
</Td>
{renderMetadata(auditLog.event)}
</Tr>
);
}
const formattedDate = `${day}-${month}-${year} at ${hours}:${minutes} ${period}`;
return formattedDate;
};
return (
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
<Td>{formatDate(auditLog.createdAt)}</Td>
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
{renderActor(auditLog.actor)}
<Td>
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
<p>{auditLog.ipAddress}</p>
</Td>
{renderMetadata(auditLog.event)}
</Tr>
);
};
@@ -1,28 +1,20 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent
} from "@app/components/v2";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useAddTrustedIp,
useGetMyIp,
useUpdateTrustedIp
} from "@app/hooks/api";
import { useAddTrustedIp, useGetMyIp, useUpdateTrustedIp } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({
const schema = yup
.object({
ipAddress: yup.string().required("IP address is required"),
comment: yup.string()
}).required();
})
.required();
export type FormData = yup.InferType<typeof schema>;
@@ -32,162 +24,146 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["trustedIp"]>, state?: boolean) => void;
};
export const IPAllowlistModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { createNotification } = useNotificationContext();
const { data, isLoading } = useGetMyIp();
const { currentWorkspace } = useWorkspace();
const addTrustedIp = useAddTrustedIp();
const updateTrustedIp = useUpdateTrustedIp();
export const IPAllowlistModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { createNotification } = useNotificationContext();
const { data, isLoading } = useGetMyIp();
const {
control,
setValue,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema)
});
useEffect(() => {
const trustedIpData = popUp?.trustedIp?.data as {
ipAddress: string;
comment: string;
prefix: number;
};
const { currentWorkspace } = useWorkspace();
const addTrustedIp = useAddTrustedIp();
const updateTrustedIp = useUpdateTrustedIp();
if (popUp?.trustedIp?.data) {
reset({
ipAddress: `${trustedIpData.ipAddress}${trustedIpData.prefix !== undefined ? `/${trustedIpData.prefix}` : ""}`,
comment: trustedIpData.comment
});
} else {
reset({
ipAddress: "",
comment: ""
});
}
}, [popUp?.trustedIp?.data]);
const onIPAllowlistModalSubmit = async ({
ipAddress,
comment
}: FormData) => {
try {
if (!currentWorkspace?.id) return;
if (popUp?.trustedIp?.data) {
await updateTrustedIp.mutateAsync({
workspaceId: currentWorkspace.id,
trustedIpId: (popUp?.trustedIp?.data as { trustedIpId: string })?.trustedIpId,
ipAddress,
comment,
isActive: true
});
} else {
await addTrustedIp.mutateAsync({
workspaceId: currentWorkspace.id,
ipAddress,
comment,
isActive: true
});
}
createNotification({
text: `Successfully ${popUp?.trustedIp?.data ? "updated" : "added"} trusted IP`,
type: "success"
});
reset();
handlePopUpClose("trustedIp");
} catch (err) {
createNotification({
text: `Failed to ${popUp?.trustedIp?.data ? "update" : "add"} trusted IP`,
type: "error"
});
}
const {
control,
setValue,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema)
});
useEffect(() => {
const trustedIpData = popUp?.trustedIp?.data as {
ipAddress: string;
comment: string;
prefix: number;
};
if (popUp?.trustedIp?.data) {
reset({
ipAddress: `${trustedIpData.ipAddress}${
trustedIpData.prefix !== undefined ? `/${trustedIpData.prefix}` : ""
}`,
comment: trustedIpData.comment
});
} else {
reset({
ipAddress: "",
comment: ""
});
}
}, [popUp?.trustedIp?.data]);
return (
<Modal
isOpen={popUp?.trustedIp?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("trustedIp", isOpen);
reset();
}}
>
<ModalContent title={popUp?.trustedIp?.data ? "Update IP" : "Add IP"}>
<form onSubmit={handleSubmit(onIPAllowlistModalSubmit)}>
<Controller
control={control}
defaultValue=""
name="ipAddress"
render={({ field, fieldState: { error } }) => (
<FormControl
label="IPv4/IPv6 Address / CIDR Notation"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="123.456.789.0"
/>
</FormControl>
)}
/>
{!isLoading && data && (
<Button
colorSchema="secondary"
type="button"
onClick={() => setValue("ipAddress", data)}
className="mb-8"
>
Add current IP address
</Button>
)}
<Controller
control={control}
defaultValue=""
name="comment"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Comment"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="My IP address"
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.trustedIp?.data ? "Update" : "Add"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("trustedIp")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
const onIPAllowlistModalSubmit = async ({ ipAddress, comment }: FormData) => {
try {
if (!currentWorkspace?.id) return;
if (popUp?.trustedIp?.data) {
await updateTrustedIp.mutateAsync({
workspaceId: currentWorkspace.id,
trustedIpId: (popUp?.trustedIp?.data as { trustedIpId: string })?.trustedIpId,
ipAddress,
comment,
isActive: true
});
} else {
await addTrustedIp.mutateAsync({
workspaceId: currentWorkspace.id,
ipAddress,
comment,
isActive: true
});
}
createNotification({
text: `Successfully ${popUp?.trustedIp?.data ? "updated" : "added"} trusted IP`,
type: "success"
});
reset();
handlePopUpClose("trustedIp");
} catch (err) {
createNotification({
text: `Failed to ${popUp?.trustedIp?.data ? "update" : "add"} trusted IP`,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.trustedIp?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("trustedIp", isOpen);
reset();
}}
>
<ModalContent title={popUp?.trustedIp?.data ? "Update IP" : "Add IP"}>
<form onSubmit={handleSubmit(onIPAllowlistModalSubmit)}>
<Controller
control={control}
defaultValue=""
name="ipAddress"
render={({ field, fieldState: { error } }) => (
<FormControl
label="IPv4/IPv6 Address / CIDR Notation"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="123.456.789.0" />
</FormControl>
)}
/>
{!isLoading && data && (
<Button
colorSchema="secondary"
type="button"
onClick={() => setValue("ipAddress", data)}
className="mb-8"
>
Add current IP address
</Button>
)}
<Controller
control={control}
defaultValue=""
name="comment"
render={({ field, fieldState: { error } }) => (
<FormControl label="Comment" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="My IP address" />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.trustedIp?.data ? "Update" : "Add"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("trustedIp")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -53,9 +53,9 @@ export const IPAllowlistSection = () => {
};
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">IP Allowlist</h2>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex items-center">
<h2 className="flex-1 text-xl font-semibold text-white">IP Allowlist</h2>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.IpAllowList}
@@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { faMagnifyingGlass, faPlus, faUsers,faXmark } from "@fortawesome/free-solid-svg-icons";
import { faMagnifyingGlass, faPlus, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -40,7 +40,8 @@ import {
useOrganization,
useSubscription,
useUser,
useWorkspace} from "@app/context";
useWorkspace
} from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useAddUserToWs,
@@ -50,7 +51,8 @@ import {
useGetUserWsKey,
useGetWorkspaceUsers,
useUpdateUserWorkspaceRole,
useUploadWsKey} from "@app/hooks/api";
useUploadWsKey
} from "@app/hooks/api";
const addMemberFormSchema = z.object({
orgMembershipId: z.string().trim()
@@ -67,9 +69,9 @@ export const MemberListTab = () => {
const { currentWorkspace } = useWorkspace();
const { user } = useUser();
const userId = user?._id || "";
const orgId = currentOrg?._id || "";
const workspaceId = currentWorkspace?._id || "";
const userId = user?.id || "";
const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || "";
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
orgId,
@@ -79,7 +81,7 @@ export const MemberListTab = () => {
const { data: wsKey } = useGetUserWsKey(workspaceId);
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
const { data: orgUsers } = useGetOrgUsers(orgId);
const [searchMemberFilter, setSearchMemberFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -101,7 +103,7 @@ export const MemberListTab = () => {
const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole();
const onAddMember = async ({ orgMembershipId }: TAddMemberForm) => {
if (!currentOrg?._id) return;
if (!currentOrg?.id) return;
// TODO(akhilmhdh): Move to memory storage
const userPrivateKey = localStorage.getItem("PRIVATE_KEY");
if (!userPrivateKey || !wsKey) {
@@ -110,7 +112,7 @@ export const MemberListTab = () => {
});
return;
}
const orgUser = (orgUsers || []).find(({ _id }) => _id === orgMembershipId);
const orgUser = (orgUsers || []).find(({ id }) => id === orgMembershipId);
if (!orgUser) return;
try {
@@ -137,7 +139,7 @@ export const MemberListTab = () => {
const handleRemoveUser = async () => {
const membershipId = (popUp?.removeMember?.data as { id: string })?.id;
if (!currentOrg?._id) return;
if (!currentOrg?.id) return;
try {
await removeUserFromWorkspace(membershipId);
@@ -156,30 +158,30 @@ export const MemberListTab = () => {
};
const isIamOwner = useMemo(
() => members?.find(({ user: u }) => userId === u?._id)?.role === "owner",
() => members?.find(({ user: u }) => userId === u?.id)?.role === "owner",
[userId, members]
);
const findRoleFromId = useCallback(
(roleId: string) => {
return (roles || []).find(({ _id: id }) => id === roleId);
return (roles || []).find(({ id }) => id === roleId);
},
[roles]
);
const onRoleChange = async (membershipId: string, role: string) => {
if (!currentOrg?._id) return;
if (!currentOrg?.id) return;
try {
const isCustomRole = !["admin", "member", "viewer"].includes(role);
if (isCustomRole && subscription && !subscription?.rbac) {
handlePopUpOpen("upgradePlan", {
description: "You can assign custom roles to members if you upgrade your Infisical plan."
});
return;
}
await updateUserWorkspaceRole({ membershipId, role });
createNotification({
text: "Successfully updated user role",
@@ -215,7 +217,7 @@ export const MemberListTab = () => {
({ status, user: u }) => status === "accepted" && !wsUserEmails.has(u.email)
);
}, [orgUsers, members]);
const onGrantAccess = async (grantedUserId: string, publicKey: string) => {
try {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
@@ -239,7 +241,7 @@ export const MemberListTab = () => {
userId: grantedUserId,
nonce,
encryptedKey: ciphertext,
workspaceId: currentWorkspace?._id || ""
workspaceId: currentWorkspace?.id || ""
});
} catch (err) {
console.error(err);
@@ -254,25 +256,20 @@ export const MemberListTab = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between items-center mb-4">
<p className="text-xl font-semibold text-mineshaft-100">
Members
</p>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addMember")}
isDisabled={!isAllowed}
>
Add Member
</Button>
)}
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Members</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addMember")}
isDisabled={!isAllowed}
>
Add Member
</Button>
)}
</ProjectPermissionCan>
</div>
<Input
@@ -296,7 +293,7 @@ export const MemberListTab = () => {
{isLoading && <TableSkeleton columns={4} innerKey="project-members" />}
{!isLoading &&
filterdUsers?.map(
({ user: u, inviteEmail, _id: membershipId, status, customRole, role }) => {
({ user: u, inviteEmail, id: membershipId, status, customRole, role }) => {
const name = u ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail;
@@ -315,7 +312,7 @@ export const MemberListTab = () => {
value={
role === "custom" ? findRoleFromId(customRole)?.slug : role
}
isDisabled={userId === u?._id || !isAllowed}
isDisabled={userId === u?.id || !isAllowed}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
@@ -337,7 +334,7 @@ export const MemberListTab = () => {
<Button
colorSchema="secondary"
isDisabled={!isAllowed}
onClick={() => onGrantAccess(u?._id, u?.publicKey)}
onClick={() => onGrantAccess(u?.id, u?.publicKey)}
>
Grant Access
</Button>
@@ -348,7 +345,7 @@ export const MemberListTab = () => {
</ProjectPermissionCan>
</Td>
<Td>
{userId !== u?._id && (
{userId !== u?.id && (
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Member}
@@ -360,7 +357,7 @@ export const MemberListTab = () => {
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={userId === u?._id || !isAllowed}
isDisabled={userId === u?.id || !isAllowed}
onClick={() =>
handlePopUpOpen("removeMember", { id: membershipId })
}
@@ -405,7 +402,7 @@ export const MemberListTab = () => {
value={field.value}
onValueChange={field.onChange}
>
{filteredOrgUsers.map(({ _id: orgUserId, user: u }) => (
{filteredOrgUsers.map(({ id: orgUserId, user: u }) => (
<SelectItem value={orgUserId} key={`org-membership-join-${orgUserId}`}>
{u?.email}
</SelectItem>
@@ -38,9 +38,9 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?._id || "";
const workspaceId = currentWorkspace?._id || "";
const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || "";
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
orgId,
workspaceId
@@ -49,7 +49,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
const { mutateAsync: deleteRole } = useDeleteRole();
const handleRoleDelete = async () => {
const { _id: id } = popUp?.deleteRole?.data as TRole<string>;
const { id } = popUp?.deleteRole?.data as TRole<string>;
try {
await deleteRole({
orgId,
@@ -100,7 +100,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
<TBody>
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{(roles as TRole<string>[])?.map((role) => {
const { _id: id, name, slug } = role;
const { id, name, slug } = role;
const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug);
return (
@@ -108,7 +108,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>
<div className="flex space-x-2 justify-end">
<div className="flex justify-end space-x-2">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Role}
@@ -121,9 +121,9 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const orgId = currentOrg?._id || "";
const orgId = currentOrg?.id || "";
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || "";
const workspaceId = currentWorkspace?.id || "";
const {
handleSubmit,
@@ -139,12 +139,12 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
const { mutateAsync: updateRole } = useUpdateRole();
const handleRoleUpdate = async (el: TFormSchema) => {
if (!role?._id) return;
if (!role?.id) return;
try {
await updateRole({
orgId,
id: role?._id,
id: role?.id,
workspaceId,
...el,
permissions: formRolePermission2API(el.permissions)
@@ -1,47 +1,44 @@
import { faKey, faPencil,faXmark } from "@fortawesome/free-solid-svg-icons";
import { faKey, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
EmptyState,
IconButton,
Switch,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
EmptyState,
IconButton,
Switch,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub , useWorkspace } from "@app/context";
import {
useGetWorkspaceServiceTokenDataV3,
useUpdateServiceTokenV3
} from "@app/hooks/api";
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useGetWorkspaceServiceTokenDataV3, useUpdateServiceTokenV3 } from "@app/hooks/api";
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteServiceTokenV3", "serviceTokenV3"]>,
data?: {
serviceTokenDataId?: string;
name?: string;
role?: string;
customRole?: {
name: string;
slug: string;
};
trustedIps?: ServiceTokenV3TrustedIp[];
accessTokenTTL?: number;
isRefreshTokenRotationEnabled?: boolean;
}
) => void;
};
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteServiceTokenV3", "serviceTokenV3"]>,
data?: {
serviceTokenDataId?: string;
name?: string;
role?: string;
customRole?: {
name: string;
slug: string;
};
trustedIps?: ServiceTokenV3TrustedIp[];
accessTokenTTL?: number;
isRefreshTokenRotationEnabled?: boolean;
}
) => void;
};
export const ServiceTokenV3Table = ({
handlePopUpOpen
@@ -51,30 +48,29 @@ export const ServiceTokenV3Table = ({
const { data, isLoading } = useGetWorkspaceServiceTokenDataV3(currentWorkspace?.id || "");
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
const handleToggleServiceTokenDataStatus = async ({
const handleToggleServiceTokenDataStatus = async ({
serviceTokenDataId,
isActive
}: {
serviceTokenDataId: string;
isActive: boolean;
}) => {
try {
await updateMutateAsync({
serviceTokenDataId,
isActive
}: {
serviceTokenDataId: string;
isActive: boolean;
}) => {
try {
await updateMutateAsync({
serviceTokenDataId,
isActive
});
});
createNotification({
text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`,
type: "success"
});
} catch (err) {
console.log(err);
createNotification({
text: `Failed to ${isActive ? "enable" : "disable"} service token v3`,
type: "error"
});
}
createNotification({
text: `Successfully ${isActive ? "enabled" : "disabled"} service token v3`,
type: "success"
});
} catch (err) {
console.log(err);
createNotification({
text: `Failed to ${isActive ? "enable" : "disable"} service token v3`,
type: "error"
});
}
return (
@@ -203,16 +199,76 @@ export const ServiceTokenV3Table = ({
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={7}>
<EmptyState title="No service token v3 on file" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
})}
</Td>
<Td>{accessTokenTTL}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td>
<Td className="flex justify-end">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.ServiceTokens}
>
{(isAllowed) => (
<IconButton
onClick={async () => {
handlePopUpOpen("serviceTokenV3", {
serviceTokenDataId: id,
name,
role,
customRole,
trustedIps,
accessTokenTTL,
isRefreshTokenRotationEnabled
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.ServiceTokens}
>
{(isAllowed) => (
<IconButton
onClick={() => {
handlePopUpOpen("deleteServiceTokenV3", {
serviceTokenDataId: id,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</ProjectPermissionCan>
</Td>
</Tr>
);
}
)}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={7}>
<EmptyState title="No service token v3 on file" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -18,18 +18,25 @@ export const SecretApprovalPage = () => {
const workspaceId = currentWorkspace?.id || "";
return (
<div className="container mx-auto bg-bunker-800 text-white w-full h-full max-w-7xl px-6">
<div className="py-6 flex justify-between items-center">
<div className="flex flex-col w-full">
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 px-6 text-white">
<div className="flex items-center justify-between py-6">
<div className="flex w-full flex-col">
<h2 className="text-3xl font-semibold text-gray-200">Secret Approval Workflows</h2>
<p className="text-bunker-300">Create approval policies for any modifications to secrets in sensitive environments and folders.</p>
<p className="text-bunker-300">
Create approval policies for any modifications to secrets in sensitive environments and
folders.
</p>
</div>
<div className="flex justify-center w-max">
<div className="flex w-max justify-center">
<Link href="https://infisical.com/docs/documentation/platform/pr-workflows">
<span className="rounded-md px-4 py-2 w-max text-mineshaft-200 hover:text-white bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/10 hover:border-primary/40 duration-200 cursor-pointer">
Documentation <FontAwesomeIcon icon={faArrowUpRightFromSquare} className="text-xs mb-[0.06rem] ml-1"/>
<span className="w-max cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
</span>
</Link>
</Link>
</div>
</div>
<Tabs defaultValue={TabSection.ApprovalRequests}>
@@ -59,7 +59,7 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
const handleDeletePolicy = async () => {
const { id: id } = popUp.deletePolicy.data as TSecretApprovalPolicy;
const { id } = popUp.deletePolicy.data as TSecretApprovalPolicy;
try {
await deleteSecretApprovalPolicy({
workspaceId,
@@ -81,10 +81,10 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
return (
<div>
<div className="flex justify-between mb-6">
<div className="mb-6 flex justify-between">
<div className="flex flex-col">
<span className="text-xl font-semibold text-mineshaft-100">Approval Policies</span>
<div className="text-bunker-300 mt-2 text-sm">
<div className="mt-2 text-sm text-bunker-300">
Implement policies to prevent unauthorized secret changes.
</div>
</div>
@@ -50,7 +50,7 @@ export const SecretApprovalPolicyRow = ({
updateSecretApprovalPolicy(
{
workspaceId,
id: policy._id,
id: policy.id,
approvers: selectedApprovers
},
{
@@ -81,18 +81,20 @@ export const SecretApprovalPolicyRow = ({
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
align="start"
>
<DropdownMenuLabel>Select members that are allowed to approve changes</DropdownMenuLabel>
{members?.map(({ _id, user }) => {
const isChecked = selectedApprovers.includes(_id);
<DropdownMenuLabel>
Select members that are allowed to approve changes
</DropdownMenuLabel>
{members?.map(({ id, user }) => {
const isChecked = selectedApprovers.includes(id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
setSelectedApprovers((state) =>
isChecked ? state.filter((el) => el !== _id) : [...state, _id]
isChecked ? state.filter((el) => el !== id) : [...state, id]
);
}}
key={`create-policy-members-${_id}`}
key={`create-policy-members-${id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
@@ -98,10 +98,10 @@ export const SecretPolicyForm = ({
};
const handleUpdatePolicy = async (data: TFormSchema) => {
if (!editValues?._id) return;
if (!editValues?.id) return;
try {
await updateSecretApprovalPolicy({
id: editValues?._id,
id: editValues?.id,
...data,
secretPath: data.secretPath || null,
workspaceId
@@ -199,20 +199,20 @@ export const SecretPolicyForm = ({
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
align="start"
>
<DropdownMenuLabel>Select members that are allowed to approve changes</DropdownMenuLabel>
{members.map(({ _id, user }) => {
const isChecked = value?.includes(_id);
<DropdownMenuLabel>
Select members that are allowed to approve changes
</DropdownMenuLabel>
{members.map(({ id, user }) => {
const isChecked = value?.includes(id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked
? value?.filter((el) => el !== _id)
: [...(value || []), _id]
isChecked ? value?.filter((el) => el !== id) : [...(value || []), id]
);
}}
key={`create-policy-members-${_id}`}
key={`create-policy-members-${id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
@@ -243,7 +243,7 @@ export const SecretPolicyForm = ({
</FormControl>
)}
/>
<div className="flex mt-8 space-x-4 items-center">
<div className="mt-8 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
Save
</Button>
@@ -34,7 +34,7 @@ import {
export const SecretApprovalRequest = () => {
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || "";
const workspaceId = currentWorkspace?.id || "";
const [selectedApproval, setSelectedApproval] = useState<TSecretApprovalRequest | null>(null);
// filters
@@ -60,10 +60,10 @@ export const SecretApprovalRequest = () => {
const { user: presentUser } = useUser();
const { data: members } = useGetWorkspaceUsers(workspaceId);
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
(prev, curr) => ({ ...prev, [curr._id]: curr }),
(prev, curr) => ({ ...prev, [curr.id]: curr }),
{}
);
const myMembershipId = members?.find(({ user }) => user._id === presentUser?._id)?._id;
const myMembershipId = members?.find(({ user }) => user.id === presentUser.id)?.id;
const isSecretApprovalScreen = Boolean(selectedApproval);
const handleGoBackSecretRequestDetail = () => {
@@ -87,7 +87,7 @@ export const SecretApprovalRequest = () => {
<SecretApprovalRequestChanges
workspaceId={workspaceId}
members={membersGroupById}
approvalRequestId={selectedApproval?._id || ""}
approvalRequestId={selectedApproval?.id || ""}
onGoBack={handleGoBackSecretRequestDetail}
committer={membersGroupById?.[selectedApproval?.committer || ""]}
/>
@@ -169,13 +169,11 @@ export const SecretApprovalRequest = () => {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Select an author</DropdownMenuLabel>
{members?.map(({ user, _id }) => (
{members?.map(({ user, id }) => (
<DropdownMenuItem
onClick={() =>
setCommitterFilter((state) => (state === _id ? undefined : _id))
}
key={`request-filter-member-${_id}`}
icon={committerFilter === _id && <FontAwesomeIcon icon={faCheckCircle} />}
onClick={() => setCommitterFilter((state) => (state === id ? undefined : id))}
key={`request-filter-member-${id}`}
icon={committerFilter === id && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
{user.email}
@@ -195,7 +193,7 @@ export const SecretApprovalRequest = () => {
<Fragment key={`secret-approval-request-${i + 1}`}>
{group?.map((secretApproval) => {
const {
_id: reqId,
id: reqId,
commits,
committer,
createdAt,
@@ -58,17 +58,17 @@ export const SecretApprovalRequestChangeItem = ({
const hasConflict = Boolean(itemConflict);
return (
<div className="bg-bunker-500 rounded-lg pt-2 pb-4 px-4">
<div className="py-3 px-1 flex items-center">
<div className="rounded-lg bg-bunker-500 px-4 pt-2 pb-4">
<div className="flex items-center py-3 px-1">
<div className="flex-grow">{generateItemTitle(op)}</div>
{!hasMerged && isStale && (
<div className="flex items-center">
<FontAwesomeIcon icon={faInfo} className="text-primary-600 text-sm" />
<span className="text-xs ml-2">Secret has been changed(stale)</span>
<FontAwesomeIcon icon={faInfo} className="text-sm text-primary-600" />
<span className="ml-2 text-xs">Secret has been changed(stale)</span>
</div>
)}
{hasMerged && hasConflict && (
<div className="flex items-center text-sm text-bunker-300 space-x-2">
<div className="flex items-center space-x-2 text-sm text-bunker-300">
<Tooltip content="Merge Conflict">
<FontAwesomeIcon icon={faExclamationTriangle} className="text-red-700" />
</Tooltip>
@@ -99,11 +99,11 @@ export const SecretApprovalRequestChangeItem = ({
<Td>
{secretVersion?.tags?.map(({ name, id: tagId, tagColor }) => (
<Tag
className="flex items-center space-x-2 w-min"
className="flex w-min items-center space-x-2"
key={`${secretVersion.id}-${tagId}`}
>
<div
className="w-3 h-3 rounded-full"
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
@@ -121,11 +121,11 @@ export const SecretApprovalRequestChangeItem = ({
<Td>
{newVersion?.tags?.map(({ name, id: tagId, tagColor }) => (
<Tag
className="flex items-center space-x-2 w-min"
className="flex w-min items-center space-x-2"
key={`${newVersion.id}-${tagId}`}
>
<div
className="w-3 h-3 rounded-full"
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
@@ -153,13 +153,13 @@ export const SecretApprovalRequestChangeItem = ({
{(op === CommitType.CREATE ? newVersion?.tags : secretVersion?.tags)?.map(
({ name, id: tagId, tagColor }) => (
<Tag
className="flex items-center space-x-2 w-min"
className="flex w-min items-center space-x-2"
key={`${
op === CommitType.CREATE ? newVersion?.id : secretVersion?.id
}-${tagId}`}
>
<div
className="w-3 h-3 rounded-full"
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
@@ -107,7 +107,7 @@ export const SecretApprovalRequestChanges = ({
const myMembership = Object.values(members).find(
({ user: membershipUser }) => membershipUser.email === user.email
);
const myMembershipId = myMembership?._id || "";
const myMembershipId = myMembership?.id || "";
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.includes(myMembershipId);
const reviewedMembers = secretApprovalRequestDetails?.reviewers?.reduce<
Record<string, ApprovalStatus>
@@ -226,7 +226,7 @@ export const SecretApprovalRequestChanges = ({
secretVersion={secretVersion}
presentSecretVersionNumber={secret?.version || 0}
newVersion={newVersion}
key={`${op}-${index + 1}-${secretVersion?._id}`}
key={`${op}-${index + 1}-${secretVersion?.id}`}
/>
)
)}
@@ -234,7 +234,7 @@ export const SecretApprovalRequestChanges = ({
<div className="mt-8 flex items-center space-x-6 rounded-lg bg-mineshaft-800 px-5 py-6">
<SecretApprovalRequestAction
canApprove={canApprove}
approvalRequestId={secretApprovalRequestDetails._id}
approvalRequestId={secretApprovalRequestDetails.id}
hasMerged={hasMerged}
approvals={secretApprovalRequestDetails.policy.approvals || 0}
status={secretApprovalRequestDetails.status}
@@ -205,7 +205,7 @@ export const SecretMainPage = () => {
return (
<StoreProvider>
<div className="container flex flex-col mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<div className="container mx-auto flex h-full flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<div className="relative right-6 -top-2 mb-2 ml-6">
<NavHeader
pageName={t("dashboard.title")}
@@ -238,13 +238,13 @@ export const SecretMainPage = () => {
isSnapshotCountLoading={isSnapshotCountLoading}
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
/>
<div className="mt-3 overflow-y-auto overflow-x-hidden thin-scrollbar bg-mineshaft-800 text-left text-bunker-300 rounded-md text-sm">
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md bg-mineshaft-800 text-left text-sm text-bunker-300">
<div className="flex flex-col" id="dashboard">
{isNotEmtpy && (
<div className="flex font-medium border-b border-mineshaft-600">
<div style={{ width: "2.8rem" }} className="px-4 py-3 flex-shrink-0" />
<div className="flex border-b border-mineshaft-600 font-medium">
<div style={{ width: "2.8rem" }} className="flex-shrink-0 px-4 py-3" />
<div
className="w-80 flex-shrink-0 border-r flex items-center border-mineshaft-600 px-4 py-2"
className="flex w-80 flex-shrink-0 items-center border-r border-mineshaft-600 px-4 py-2"
role="button"
tabIndex={0}
onClick={handleSortToggle}
@@ -188,7 +188,7 @@ export const ActionBar = ({
return (
<>
<div className="flex items-center space-x-2 mt-4">
<div className="mt-4 flex items-center space-x-2">
<div className="w-2/5">
<Input
className="bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
@@ -206,7 +206,7 @@ export const ActionBar = ({
ariaLabel="Download"
className={twMerge(
"transition-all",
Object.keys(filter.tags).length && "text-primary border-primary/50"
Object.keys(filter.tags).length && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
@@ -245,7 +245,7 @@ export const ActionBar = ({
>
<div className="flex items-center">
<div
className="w-2 h-2 rounded-full mr-2"
className="mr-2 h-2 w-2 rounded-full"
style={{ background: tagColor || "#bec2c8" }}
/>
{name}
@@ -304,7 +304,7 @@ export const ActionBar = ({
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
className="rounded-r-none h-10"
className="h-10 rounded-r-none"
isDisabled={!isAllowed}
>
Add Secret
@@ -326,7 +326,7 @@ export const ActionBar = ({
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<div className="p-1.5 flex flex-col space-y-1">
<div className="flex flex-col space-y-1 p-1.5">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
@@ -374,17 +374,17 @@ export const ActionBar = ({
</div>
<div
className={twMerge(
"overflow-hidden transition-all h-0 flex-shrink-0",
"h-0 flex-shrink-0 overflow-hidden transition-all",
isMultiSelectActive && "h-16"
)}
>
<div className="text-bunker-300 flex items-center bg-mineshaft-800 mt-3.5 py-2 px-4 rounded-md border border-mineshaft-600">
<div className="mt-3.5 flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-bunker-300">
<Tooltip content="Clear">
<IconButton variant="plain" ariaLabel="clear-selection" onClick={resetSelectedSecret}>
<FontAwesomeIcon icon={faMinusSquare} size="lg" />
</IconButton>
</Tooltip>
<div className="text-sm ml-4 px-2 flex-grow">
<div className="ml-4 flex-grow px-2 text-sm">
{Object.keys(selectedSecrets).length} Selected
</div>
<ProjectPermissionCan
@@ -199,7 +199,7 @@ export const CopySecretsFromBoard = ({
<div className="border-t border-mineshaft-600 pt-4">
<div className="mb-4 flex items-center justify-between">
<div>Secrets</div>
<div className="w-1/2 flex items-center space-x-2">
<div className="flex w-1/2 items-center space-x-2">
<Input
placeholder="Search for secret"
value={searchFilter}
@@ -232,7 +232,7 @@ export const CopySecretsFromBoard = ({
{!isSecretsLoading && !secrets?.length && (
<EmptyState title="No secrets found" icon={faKey} />
)}
<div className="grid grid-cols-2 gap-4 max-h-64 overflow-auto thin-scrollbar ">
<div className="thin-scrollbar grid max-h-64 grid-cols-2 gap-4 overflow-auto ">
{isSecretsLoading &&
Array.apply(0, Array(2)).map((_x, i) => (
<Skeleton key={`secret-pull-loading-${i + 1}`} className="bg-mineshaft-700" />
@@ -165,7 +165,7 @@ export const SecretDetailSidebar = ({
>
<DrawerContent title="Secret">
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
<div className="flex flex-col h-full">
<div className="flex h-full flex-col">
<FormControl label="Key">
<Input isDisabled {...register("key")} />
</FormControl>
@@ -226,22 +226,22 @@ export const SecretDetailSidebar = ({
/>
)}
<FormControl label="Tags" className="">
<div className="overflow-hidden grid gap-2 grid-flow-col auto-cols-min pt-2">
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
{fields.map(({ tagColor, id: formId, name, id }) => (
<Tag
className="flex items-center space-x-2 w-min"
className="flex w-min items-center space-x-2"
key={formId}
onClose={() => {
if (cannotEditSecret) {
createNotification({ type: "error", text: "Access denied" });
return;
}
const tag = tags?.find(({ id: id }) => id === id);
const tag = tags?.find(({ id: tagId }) => id === tagId);
if (tag) handleTagSelect(tag);
}}
>
<div
className="w-3 h-3 rounded-full"
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
@@ -281,7 +281,7 @@ export const SecretDetailSidebar = ({
>
<div className="flex items-center">
<div
className="w-2 h-2 rounded-full mr-2"
className="mr-2 h-2 w-2 rounded-full"
style={{ background: tagColor || "#bec2c8" }}
/>
{name}
@@ -352,7 +352,7 @@ export const SecretDetailSidebar = ({
)}
/>
</div>
<div className="dark mb-4 text-sm text-bunker-300 flex-grow">
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
<div className="mb-2">Version History</div>
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, value, id }, i) => (
@@ -372,7 +372,7 @@ export const SecretDetailSidebar = ({
</div>
</div>
<div className="flex flex-col space-y-4">
<div className="flex space-x-4 items-center">
<div className="flex items-center space-x-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
@@ -72,7 +72,7 @@ export const filterSecrets = (secrets: DecryptedSecret[], filter: Filter) =>
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
const searchTerm = filter.searchFilter.toLowerCase();
return (
(!isTagFilterActive || tags.some(({ _id }) => filter.tags?.[_id])) &&
(!isTagFilterActive || tags.some(({ id }) => filter.tags?.[id])) &&
(key.toLowerCase().includes(searchTerm) || value.toLowerCase().includes(searchTerm))
);
});
@@ -191,7 +191,7 @@ export const SecretListView = ({
const handleSaveSecret = useCallback(
async (
orgSecret: DecryptedSecret,
modSecret: Omit<DecryptedSecret, "tags"> & { tags: { _id: string }[] },
modSecret: Omit<DecryptedSecret, "tags"> & { tags: { id: string }[] },
cb?: () => void
) => {
const { key: oldKey } = orgSecret;
@@ -208,8 +208,8 @@ export const SecretListView = ({
} = modSecret;
const hasKeyChanged = oldKey !== key;
const tagIds = tags.map(({ _id }) => _id);
const oldTagIds = orgSecret.tags.map(({ _id }) => _id);
const tagIds = tags.map(({ id }) => id);
const oldTagIds = orgSecret.tags.map(({ id }) => id);
const isSameTags = JSON.stringify(tagIds) === JSON.stringify(oldTagIds);
const isSharedSecUnchanged =
(
@@ -248,7 +248,7 @@ export const SecretListView = ({
comment,
reminderRepeatDays,
reminderNote,
secretId: orgSecret._id,
secretId: orgSecret.id,
newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding
});
@@ -283,7 +283,7 @@ export const SecretListView = ({
);
const handleSecretDelete = useCallback(async () => {
const { key, _id: secretId } = popUp.deleteSecret?.data as DecryptedSecret;
const { key, id: secretId } = popUp.deleteSecret?.data as DecryptedSecret;
try {
await handleSecretOperation("delete", "shared", key, { secretId });
// wrap this in another function and then reuse
@@ -346,11 +346,11 @@ export const SecretListView = ({
environment={environment}
secretPath={secretPath}
tags={wsTags}
isSelected={selectedSecrets?.[secret._id]}
isSelected={selectedSecrets?.[secret.id]}
onToggleSecretSelect={toggleSelectedSecret}
isVisible={isVisible}
secret={secret}
key={secret._id}
key={secret.id}
onSaveSecret={handleSaveSecret}
onDeleteSecret={onDeleteSecret}
onDetailViewSecret={onDetailViewSecret}
@@ -60,7 +60,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
return (
<>
<div
className="flex group border-b border-mineshaft-600 hover:bg-mineshaft-700 cursor-pointer"
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={setIsExpanded.toggle}
@@ -74,17 +74,17 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<div className="w-12 flex-shrink-0 px-4 py-3">
<FontAwesomeIcon icon={faKey} />
</div>
<div className="flex-grow px-4 py-3 flex items-center space-x-4">
<div className="flex flex-grow items-center space-x-4 px-4 py-3">
{mode === "modified" ? (
<>
<div>{preSecret?.key}</div>
<div className="bg-primary text-black rounded-lg font-bold px-1 py-0.5 text-xs">
<div className="rounded-lg bg-primary px-1 py-0.5 text-xs font-bold text-black">
v{preSecret?.version}
</div>
<div>
<FontAwesomeIcon icon={faChevronRight} size="sm" className="text-orange-700" />
</div>
<div className="bg-primary text-black rounded-lg font-bold px-1 py-0.5 text-xs">
<div className="rounded-lg bg-primary px-1 py-0.5 text-xs font-bold text-black">
v{postSecret?.version}
</div>
</>
@@ -94,7 +94,7 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
</div>
</div>
{isExpanded && (
<div className="flex bg-bunker-600 cursor-pointer p-4">
<div className="flex cursor-pointer bg-bunker-600 p-4">
<TableContainer>
<Table>
<THead>
@@ -153,11 +153,11 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<Td className="border-r border-mineshaft-600">
{preSecret?.tags?.map(({ name, id: tagId, tagColor }) => (
<Tag
className="flex items-center space-x-2 w-min"
className="flex w-min items-center space-x-2"
key={`${preSecret.id}-${tagId}`}
>
<div
className="w-3 h-3 rounded-full"
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
@@ -168,11 +168,11 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<Td>
{postSecret?.tags?.map(({ name, id: tagId, tagColor }) => (
<Tag
className="flex items-center space-x-2 w-min"
className="flex w-min items-center space-x-2"
key={`${postSecret.id}-${tagId}`}
>
<div
className="w-3 h-3 rounded-full"
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{name}</div>
@@ -98,7 +98,7 @@ export const SnapshotView = ({
);
const diffView: Array<TDiffView<DecryptedSecret>> = [];
rollingSecrets.forEach((rollSecret) => {
const { id: id } = rollSecret;
const { id } = rollSecret;
const doesExist = Boolean(secretGroupById?.[id]);
if (doesExist) {
diffView.push({
@@ -154,11 +154,11 @@ export const SnapshotView = ({
return (
<>
<div className="flex space-x-4 items-center">
<div className="flex items-center space-x-4">
<h6 className="text-2xl">Snapshot</h6>
<Tag colorSchema="green">{new Date(snapshotData?.createdAt || "").toLocaleString()}</Tag>
</div>
<div className="flex items-center space-x-2 mt-4">
<div className="mt-4 flex items-center space-x-2">
<div className="w-2/5">
<Input
className="bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
@@ -210,19 +210,19 @@ export const SnapshotView = ({
</ProjectPermissionCan>
</div>
</div>
<div className="mt-4 bg-mineshaft-800 text-left text-bunker-300 rounded-md text-sm">
<div className="mt-4 rounded-md bg-mineshaft-800 text-left text-sm text-bunker-300">
<div className="flex flex-col ">
<div className="flex font-medium border-b border-mineshaft-600">
<div className="flex border-b border-mineshaft-600 font-medium">
<div className="w-12 flex-shrink-0" />
<div className="w-12 flex-shrink-0" />
<div className="flex-grow px-4 py-2 flex items-center">Changes</div>
<div className="flex flex-grow items-center px-4 py-2">Changes</div>
</div>
{folderDiffView
.sort((a, b) => a.post.name.toLowerCase().localeCompare(b.post.name.toLowerCase()))
.filter((a) => a.post.name.toLowerCase().includes(search.toLowerCase()))
.map(({ mode, pre, post }, index) => (
<div
className="flex group border-b border-mineshaft-600 hover:bg-mineshaft-700 cursor-pointer"
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
key={`folder-${mode}-${index + 1}`}
>
<div className="w-12 flex-shrink-0 px-4 py-3">
@@ -231,7 +231,7 @@ export const SnapshotView = ({
<div className="w-12 flex-shrink-0 px-4 py-3">
<FontAwesomeIcon icon={faFolder} />
</div>
<div className="flex-grow px-4 py-3 flex items-center space-x-4">
<div className="flex flex-grow items-center space-x-4 px-4 py-3">
{mode === "modified" ? (
<>
<div>{pre?.name}</div>
@@ -182,22 +182,28 @@ export const SecretRotationPage = withProjectPermission(
};
return (
<div className="container mx-auto bg-bunker-800 text-white w-full h-full max-w-7xl px-6">
<div className="py-6 flex justify-between items-center">
<div className="flex flex-col w-full">
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 px-6 text-white">
<div className="flex items-center justify-between py-6">
<div className="flex w-full flex-col">
<h2 className="text-3xl font-semibold text-gray-200">Secret Rotation</h2>
<p className="text-bunker-300">Stop manually rotating secrets and automate credential rotation.</p>
<p className="text-bunker-300">
Stop manually rotating secrets and automate credential rotation.
</p>
</div>
<div className="flex justify-center w-max">
<div className="flex w-max justify-center">
<Link href="https://infisical.com/docs/documentation/platform/secret-rotation/overview">
<span className="rounded-md px-4 py-2 w-max text-mineshaft-200 hover:text-white bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/10 hover:border-primary/40 duration-200 cursor-pointer">
Documentation <FontAwesomeIcon icon={faArrowUpRightFromSquare} className="text-xs mb-[0.06rem] ml-1"/>
<span className="w-max cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
</span>
</Link>
</Link>
</div>
</div>
<div className="mb-6">
<div className="text-xl font-semibold text-gray-200 mb-2 mt-6">Rotated Secrets</div>
<div className="mb-2 mt-6 text-xl font-semibold text-gray-200">Rotated Secrets</div>
<div className="flex flex-col space-y-2">
<TableContainer>
<Table>
@@ -249,9 +255,9 @@ export const SecretRotationPage = withProjectPermission(
.toUpperCase()}
</Td>
<Td>
<div className="flex items-center border border-bunker-400 rounded p-1 px-2 w-min">
<div className="flex w-min items-center rounded border border-bunker-400 p-1 px-2">
<div>{environment}</div>
<div className="flex items-center border-l border-bunker-400 pl-1 ml-1 text-xs">
<div className="ml-1 flex items-center border-l border-bunker-400 pl-1 text-xs">
<FontAwesomeIcon icon={faFolder} className="mr-1" />
{secretPath}
</div>
@@ -278,7 +284,7 @@ export const SecretRotationPage = withProjectPermission(
: "-"}
</Td>
<Td>
<div className="flex space-x-2 justify-end">
<div className="flex justify-end space-x-2">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SecretRotation}
@@ -313,7 +319,7 @@ export const SecretRotationPage = withProjectPermission(
colorSchema="danger"
ariaLabel="delete-rotation"
isDisabled={isDeleting || !isAllowed}
onClick={() => handlePopUpOpen("deleteRotation", { id: id })}
onClick={() => handlePopUpOpen("deleteRotation", { id })}
>
{isDeleting ? (
<Spinner size="xs" />
@@ -334,8 +340,10 @@ export const SecretRotationPage = withProjectPermission(
</TableContainer>
</div>
</div>
<div className="text-xl font-semibold text-gray-200 mb-2 mt-12">Infisical Rotation Providers</div>
<div className="grid grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-4">
<div className="mb-2 mt-12 text-xl font-semibold text-gray-200">
Infisical Rotation Providers
</div>
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
{isRotationProviderLoading &&
Array.from({ length: 12 }).map((_, index) => (
<Skeleton className="h-32" key={`rotation-provider-skeleton-${index + 1}`} />
@@ -343,7 +351,7 @@ export const SecretRotationPage = withProjectPermission(
{!isRotationProviderLoading &&
secretRotationProviders?.providers.map((provider) => (
<div
className="group relative cursor-pointer h-32 flex flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 hover:border-primary/40 hover:bg-primary/10"
className="group relative flex h-32 cursor-pointer flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 hover:border-primary/40 hover:bg-primary/10"
key={`infisical-rotation-provider-${provider.name}`}
tabIndex={0}
role="button"
@@ -361,21 +369,21 @@ export const SecretRotationPage = withProjectPermission(
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{provider.title}
</div>
<div className="group-hover:opacity-100 transition-all opacity-0 absolute top-1 right-1.5">
<div className="absolute top-1 right-1.5 opacity-0 transition-all group-hover:opacity-100">
<Tooltip content={provider.description} sideOffset={10}>
<FontAwesomeIcon icon={faInfoCircle} className="text-primary" />
</Tooltip>
</div>
</div>
))}
<Link href="https://github.com/Infisical/infisical/issues">
<div className="group relative cursor-pointer h-32 flex flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 hover:border-primary/40 hover:bg-primary/10">
<FontAwesomeIcon icon={faPlus} className="text-gray-300 text-3xl pl-3 pr-2" />
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
Request or create your own template
</div>
<Link href="https://github.com/Infisical/infisical/issues">
<div className="group relative flex h-32 cursor-pointer flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4 hover:border-primary/40 hover:bg-primary/10">
<FontAwesomeIcon icon={faPlus} className="pl-3 pr-2 text-3xl text-gray-300" />
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
Request or create your own template
</div>
</Link>
</div>
</Link>
</div>
<CreateRotationForm
isOpen={popUp.createRotation.isOpen}
@@ -97,9 +97,9 @@ export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Pr
</FormControl>
)}
/>
<div className="flex flex-col mt-4 pt-4 mb-2 border-t border-bunker-300/30">
<div className="mt-4 mb-2 flex flex-col border-t border-bunker-300/30 pt-4">
<div>Mapping</div>
<div className="text-bunker-300 text-sm">Select keys for rotated value to get saved</div>
<div className="text-sm text-bunker-300">Select keys for rotated value to get saved</div>
</div>
{Object.keys(outputSchema).map((outputName) => (
<Controller
@@ -117,8 +117,7 @@ export const RotationOutputForm = ({ onSubmit, onCancel, outputSchema = {} }: Pr
{!isSecretsLoading &&
secrets
?.filter(
({ id }) =>
value === id || !Object.values(selectedSecrets || {}).includes(id)
({ id }) => value === id || !Object.values(selectedSecrets || {}).includes(id)
)
?.map(({ key, id }) => (
<SelectItem value={id} key={id}>
@@ -71,7 +71,7 @@ export const PreviewSection = () => {
console.error(err);
}
};
return (
<div>
{subscription &&
@@ -79,10 +79,15 @@ export const PreviewSection = () => {
subscription?.slug !== "pro" &&
subscription?.slug !== "pro-annual" && (
<div className="flex flex-row space-x-6">
<div className="p-4 rounded-lg flex-1 border border-primary/40 mb-6 flex items-center bg-primary/10">
<div className="mb-6 flex flex-1 items-center rounded-lg border border-primary/40 bg-primary/10 p-4">
<div className="flex-1">
<h2 className="text-xl font-medium text-mineshaft-100">Unleash the full power of <span className="text-transparent font-semibold bg-clip-text bg-gradient-to-r from-primary-500 to-yellow">Infisical</span></h2>
<p className="text-gray-400 mt-4">
<h2 className="text-xl font-medium text-mineshaft-100">
Unleash the full power of{" "}
<span className="bg-gradient-to-r from-primary-500 to-yellow bg-clip-text font-semibold text-transparent">
Infisical
</span>
</h2>
<p className="mt-4 text-gray-400">
Get unlimited members, projects, RBAC, smart alerts, and so much more.
</p>
</div>
@@ -98,23 +103,29 @@ export const PreviewSection = () => {
)}
</OrgPermissionCan>
</div>
<div className="flex flex-col max-w-[12rem] w-full items-start border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-800 p-4 rounded-lg">
<div className="mb-4 flex justify-center w-full font-semibold text-mineshaft-200">Want to learn more? </div>
<div className="flex justify-center w-full">
<div className="mb-6 flex flex w-full max-w-[12rem] flex-col items-start items-center rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
<div className="mb-4 flex w-full justify-center font-semibold text-mineshaft-200">
Want to learn more?{" "}
</div>
<div className="flex w-full justify-center">
<Link href="https://infisical.com/schedule-demo">
<span className="rounded-full px-4 py-2 bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/10 hover:border-primary/40 duration-200 cursor-pointer">
Book a demo <FontAwesomeIcon icon={faArrowUpRightFromSquare} className="text-xs mb-[0.06rem] ml-1"/>
<span className="cursor-pointer rounded-full border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 duration-200 hover:border-primary/40 hover:bg-primary/10">
Book a demo{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
</span>
</Link>
</Link>
</div>
</div>
</div>
)}
{!isLoading && subscription && data && (
<div className="flex mb-6">
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600">
<div className="mb-6 flex">
<div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-2 text-gray-400">Current plan</p>
<p className="text-2xl text-mineshaft-50 font-semibold mb-8">
<p className="mb-8 text-2xl font-semibold text-mineshaft-50">
{`${formatPlanSlug(subscription.slug)} ${
subscription.status === "trialing" ? "(Trial)" : ""
}`}
@@ -136,17 +147,17 @@ export const PreviewSection = () => {
)}
</OrgPermissionCan>
</div>
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4">
<div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-2 text-gray-400">Price</p>
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
<p className="mb-8 text-2xl font-semibold text-mineshaft-50">
{subscription.status === "trialing"
? "$0.00 / month"
: `${formatAmount(data.amount)} / ${data.interval}`}
</p>
</div>
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600">
<div className="flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-2 text-gray-400">Subscription renews on</p>
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
<p className="mb-8 text-2xl font-semibold text-mineshaft-50">
{formatDate(data.currentPeriodEnd)}
</p>
</div>
@@ -24,7 +24,7 @@ export const CompanyNameSection = () => {
},
resolver: yupResolver(schema)
});
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
const { data } = useGetOrgBillingDetails(currentOrg?.id ?? "");
const { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
useEffect(() => {
@@ -37,11 +37,11 @@ export const CompanyNameSection = () => {
const onFormSubmit = async ({ name }: { name: string }) => {
try {
if (!currentOrg?._id) return;
if (!currentOrg?.id) return;
if (name === "") return;
await mutateAsync({
name,
organizationId: currentOrg._id
organizationId: currentOrg.id
});
createNotification({
@@ -60,9 +60,9 @@ export const CompanyNameSection = () => {
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Business name</h2>
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Business name</h2>
<div className="max-w-md">
<Controller
defaultValue=""
@@ -24,7 +24,7 @@ export const InvoiceEmailSection = () => {
},
resolver: yupResolver(schema)
});
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
const { data } = useGetOrgBillingDetails(currentOrg?.id ?? "");
const { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
useEffect(() => {
@@ -37,12 +37,12 @@ export const InvoiceEmailSection = () => {
const onFormSubmit = async ({ email }: { email: string }) => {
try {
if (!currentOrg?._id) return;
if (!currentOrg?.id) return;
if (email === "") return;
await mutateAsync({
email,
organizationId: currentOrg._id
organizationId: currentOrg.id
});
createNotification({
@@ -61,9 +61,9 @@ export const InvoiceEmailSection = () => {
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<h2 className="text-xl font-semibold flex-1 text-white mb-8">Invoice email recipient</h2>
<h2 className="mb-8 flex-1 text-xl font-semibold text-white">Invoice email recipient</h2>
<div className="max-w-md">
<Controller
defaultValue=""
@@ -13,9 +13,9 @@ export const PmtMethodsSection = () => {
const { mutateAsync, isLoading } = useAddOrgPmtMethod();
const handleAddPmtMethodBtnClick = async () => {
if (!currentOrg?._id) return;
if (!currentOrg?.id) return;
const url = await mutateAsync({
organizationId: currentOrg._id,
organizationId: currentOrg.id,
success_url: window.location.href,
cancel_url: window.location.href
});
@@ -24,9 +24,9 @@ export const PmtMethodsSection = () => {
};
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">Payment methods</h2>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex items-center">
<h2 className="flex-1 text-xl font-semibold text-white">Payment methods</h2>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
@@ -19,13 +19,13 @@ import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
export const PmtMethodsTable = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? "");
const { data, isLoading } = useGetOrgPmtMethods(currentOrg?.id ?? "");
const deleteOrgPmtMethod = useDeleteOrgPmtMethod();
const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => {
if (!currentOrg?._id) return;
if (!currentOrg?.id) return;
await deleteOrgPmtMethod.mutateAsync({
organizationId: currentOrg._id,
organizationId: currentOrg.id,
pmtMethodId
});
};
@@ -46,8 +46,8 @@ export const PmtMethodsTable = () => {
{!isLoading &&
data &&
data?.length > 0 &&
data.map(({ _id, brand, exp_month, exp_year, funding, last4 }) => (
<Tr key={`pmt-method-${_id}`} className="h-10">
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>
@@ -60,7 +60,7 @@ export const PmtMethodsTable = () => {
{(isAllowed) => (
<IconButton
onClick={async () => {
await handleDeletePmtMethodBtnClick(_id);
await handleDeletePmtMethodBtnClick(id);
}}
size="lg"
isDisabled={!isAllowed}
@@ -1,4 +1,4 @@
import { Controller, useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
@@ -69,10 +69,12 @@ const taxIDTypes = [
{ label: "South Africa VAT", value: "za_vat" }
];
const schema = yup.object({
const schema = yup
.object({
type: yup.string().required("Tax ID type is required"),
value: yup.string().required("Tax ID value is required")
}).required();
})
.required();
export type AddTaxIDFormData = yup.InferType<typeof schema>;
@@ -82,115 +84,100 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addTaxID"]>, state?: boolean) => void;
};
export const TaxIDModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const addOrgTaxId = useAddOrgTaxId();
export const TaxIDModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization();
const addOrgTaxId = useAddOrgTaxId();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<AddTaxIDFormData>({
resolver: yupResolver(schema)
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<AddTaxIDFormData>({
resolver: yupResolver(schema)
});
const onTaxIDModalSubmit = async ({ type, value }: AddTaxIDFormData) => {
try {
if (!currentOrg?._id) return;
await addOrgTaxId.mutateAsync({
organizationId: currentOrg._id,
type,
value
});
const onTaxIDModalSubmit = async ({ type, value }: AddTaxIDFormData) => {
try {
if (!currentOrg?.id) return;
await addOrgTaxId.mutateAsync({
organizationId: currentOrg.id,
type,
value
});
createNotification({
text: "Successfully added Tax ID",
type: "success"
});
handlePopUpClose("addTaxID");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to add Tax ID",
type: "error"
});
}
createNotification({
text: "Successfully added Tax ID",
type: "success"
});
handlePopUpClose("addTaxID");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to add Tax ID",
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.addTaxID?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addTaxID", isOpen);
reset();
}}
>
<ModalContent title="Add Tax ID">
<form onSubmit={handleSubmit(onTaxIDModalSubmit)}>
<Controller
control={control}
name="type"
defaultValue="eu_vat"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Type"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{taxIDTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="value"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Value"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="DE000000000"
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
return (
<Modal
isOpen={popUp?.addTaxID?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addTaxID", isOpen);
reset();
}}
>
<ModalContent title="Add Tax ID">
<form onSubmit={handleSubmit(onTaxIDModalSubmit)}>
<Controller
control={control}
name="type"
defaultValue="eu_vat"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Type" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
Add
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
{taxIDTypes.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="value"
render={({ field, fieldState: { error } }) => (
<FormControl label="Value" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="DE000000000" />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -72,13 +72,13 @@ const taxIDTypeLabelMap: { [key: string]: string } = {
export const TaxIDTable = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? "");
const { data, isLoading } = useGetOrgTaxIds(currentOrg?.id ?? "");
const deleteOrgTaxId = useDeleteOrgTaxId();
const handleDeleteTaxIdBtnClick = async (taxId: string) => {
if (!currentOrg?._id) return;
if (!currentOrg?.id) return;
await deleteOrgTaxId.mutateAsync({
organizationId: currentOrg._id,
organizationId: currentOrg.id,
taxId
});
};
@@ -97,8 +97,8 @@ export const TaxIDTable = () => {
{!isLoading &&
data &&
data?.length > 0 &&
data.map(({ _id, type, value }) => (
<Tr key={`tax-id-${_id}`} className="h-10">
data.map(({ id, type, value }) => (
<Tr key={`tax-id-${id}`} className="h-10">
<Td>{taxIDTypeLabelMap[type]}</Td>
<Td>{value}</Td>
<Td>
@@ -109,7 +109,7 @@ export const TaxIDTable = () => {
{(isAllowed) => (
<IconButton
onClick={async () => {
await handleDeleteTaxIdBtnClick(_id);
await handleDeleteTaxIdBtnClick(id);
}}
size="lg"
colorSchema="danger"
@@ -18,7 +18,7 @@ import { useGetOrgInvoices } from "@app/hooks/api";
export const InvoicesTable = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? "");
const { data, isLoading } = useGetOrgInvoices(currentOrg?.id ?? "");
return (
<TableContainer className="mt-8">
<Table>
@@ -35,7 +35,7 @@ export const InvoicesTable = () => {
{!isLoading &&
data &&
data?.length > 0 &&
data.map(({ _id, created, paid, number, total, invoice_pdf }) => {
data.map(({ id, created, paid, number, total, invoice_pdf }) => {
const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD"
@@ -47,7 +47,7 @@ export const InvoicesTable = () => {
const formattedDate: string = `${day}/${month}/${year}`;
return (
<Tr key={`invoice-${_id}`} className="h-10">
<Tr key={`invoice-${id}`} className="h-10">
<Td>{number}</Td>
<Td>{formattedDate}</Td>
<Td>{paid ? "Paid" : "Not Paid"}</Td>
@@ -12,57 +12,52 @@ import {
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgLicenses
} from "@app/hooks/api";
import { useGetOrgLicenses } from "@app/hooks/api";
export const LicensesSection = () => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgLicenses(currentOrg?.id ?? "");
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">Enterprise licenses</h2>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>License Key</Th>
<Th>Status</Th>
<Th>Issued Date</Th>
<Th>Expiry Date</Th>
</Tr>
</THead>
<TBody>
{!isLoading && data && data?.length > 0 && data.map(({
id,
licenseKey,
isActivated,
createdAt,
expiresAt
}) => {
const formattedCreatedAt = new Date(createdAt).toISOString().split("T")[0];
const formattedExpiresAt = new Date(expiresAt).toISOString().split("T")[0];
return (
<Tr key={`license-${id}`} className="h-10">
<Td>{licenseKey}</Td>
<Td>{isActivated ? "Active" : "Inactive"}</Td>
<Td>{formattedCreatedAt}</Td>
<Td>{formattedExpiresAt}</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={4} innerKey="licenses" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No enterprise licenses on file" icon={faFileContract} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
);
}
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgLicenses(currentOrg?.id ?? "");
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">Enterprise licenses</h2>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>License Key</Th>
<Th>Status</Th>
<Th>Issued Date</Th>
<Th>Expiry Date</Th>
</Tr>
</THead>
<TBody>
{!isLoading &&
data &&
data?.length > 0 &&
data.map(({ id, licenseKey, isActivated, createdAt, expiresAt }) => {
const formattedCreatedAt = new Date(createdAt).toISOString().split("T")[0];
const formattedExpiresAt = new Date(expiresAt).toISOString().split("T")[0];
return (
<Tr key={`license-${id}`} className="h-10">
<Td>{licenseKey}</Td>
<Td>{isActivated ? "Active" : "Inactive"}</Td>
<Td>{formattedCreatedAt}</Td>
<Td>{formattedExpiresAt}</Td>
</Tr>
);
})}
{isLoading && <TableSkeleton columns={4} innerKey="licenses" />}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No enterprise licenses on file" icon={faFileContract} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</div>
);
};
@@ -82,9 +82,9 @@ export const OrgSSOSection = (): JSX.Element => {
};
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">SAML SSO Configuration</h2>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex items-center">
<h2 className="flex-1 text-xl font-semibold text-white">SAML SSO Configuration</h2>
{!isLoading && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
@@ -117,24 +117,24 @@ export const OrgSSOSection = (): JSX.Element => {
</div>
)}
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
<p className="text-gray-400 text-md">{data && data.id !== "" ? data.id : "-"}</p>
<h3 className="text-sm text-mineshaft-400">SSO identifier</h3>
<p className="text-md text-gray-400">{data && data.id !== "" ? data.id : "-"}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Type</h3>
<p className="text-gray-400 text-md">
<h3 className="text-sm text-mineshaft-400">Type</h3>
<p className="text-md text-gray-400">
{data && data.authProvider !== "" ? ssoAuthProviderMap[data.authProvider] : "-"}
</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
<p className="text-gray-400 text-md">
<h3 className="text-sm text-mineshaft-400">Entrypoint</h3>
<p className="text-md text-gray-400">
{data && data.entryPoint !== "" ? data.entryPoint : "-"}
</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Issuer</h3>
<p className="text-gray-400 text-md">{data && data.issuer !== "" ? data.issuer : "-"}</p>
<h3 className="text-sm text-mineshaft-400">Issuer</h3>
<p className="text-md text-gray-400">{data && data.issuer !== "" ? data.issuer : "-"}</p>
</div>
<SSOModal
popUp={popUp}
@@ -1,44 +1,43 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
TextArea} from "@app/components/v2";
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
TextArea
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useCreateSSOConfig,
useGetSSOConfig,
useUpdateSSOConfig
} from "@app/hooks/api";
import { useCreateSSOConfig, useGetSSOConfig, useUpdateSSOConfig } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
enum AuthProvider {
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
}
const ssoAuthProviders = [
{ label: "Okta SAML", value: AuthProvider.OKTA_SAML },
{ label: "Azure SAML", value: AuthProvider.AZURE_SAML },
{ label: "JumpCloud SAML", value: AuthProvider.JUMPCLOUD_SAML }
{ label: "Okta SAML", value: AuthProvider.OKTA_SAML },
{ label: "Azure SAML", value: AuthProvider.AZURE_SAML },
{ label: "JumpCloud SAML", value: AuthProvider.JUMPCLOUD_SAML }
];
const schema = yup.object({
const schema = yup
.object({
authProvider: yup.string().required("SSO Type is required"),
entryPoint: yup.string().required("IdP entrypoint is required"),
issuer: yup.string().required("Issuer string is required"),
cert: yup.string().required("IdP's public signing certificate is required")
}).required();
})
.required();
export type AddSSOFormData = yup.InferType<typeof schema>;
@@ -48,243 +47,221 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addSSO"]>, state?: boolean) => void;
};
export const SSOModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateSSOConfig();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateSSOConfig();
const { data } = useGetSSOConfig(currentOrg?.id ?? "");
const {
control,
handleSubmit,
reset,
watch,
} = useForm<AddSSOFormData>({
defaultValues: {
authProvider: AuthProvider.OKTA_SAML
},
resolver: yupResolver(schema)
});
useEffect(() => {
if (data) {
reset({
authProvider: data?.authProvider ?? "",
entryPoint: data?.entryPoint ?? "",
issuer: data?.issuer ?? "",
cert: data?.cert ?? ""
});
}
}, [data]);
const onSSOModalSubmit = async ({
authProvider,
entryPoint,
issuer,
cert
}: AddSSOFormData) => {
try {
if (!currentOrg) return;
if (!data) {
await createMutateAsync({
organizationId: currentOrg.id,
authProvider,
isActive: false,
entryPoint,
issuer,
cert
});
} else {
await updateMutateAsync({
organizationId: currentOrg.id,
authProvider,
isActive: false,
entryPoint,
issuer,
cert
});
}
export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateSSOConfig();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateSSOConfig();
const { data } = useGetSSOConfig(currentOrg?.id ?? "");
handlePopUpClose("addSSO");
const { control, handleSubmit, reset, watch } = useForm<AddSSOFormData>({
defaultValues: {
authProvider: AuthProvider.OKTA_SAML
},
resolver: yupResolver(schema)
});
createNotification({
text: `Successfully ${!data ? "added" : "updated"} SAML SSO configuration`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${!data ? "add" : "update"} SAML SSO configuration`,
type: "error"
});
}
useEffect(() => {
if (data) {
reset({
authProvider: data?.authProvider ?? "",
entryPoint: data?.entryPoint ?? "",
issuer: data?.issuer ?? "",
cert: data?.cert ?? ""
});
}
}, [data]);
const renderLabels = (authProvider: string) => {
switch (authProvider){
case AuthProvider.OKTA_SAML:
return ({
acsUrl: "Single sign-on URL",
entityId: "Audience URI (SP Entity ID)",
entryPoint: "Identity Provider Single Sign-On URL",
entryPointPlaceholder: "https://your-domain.okta.com/app/app-name/xxx/sso/saml",
issuer: "Identity Provider Issuer",
issuerPlaceholder: "http://www.okta.com/xxx"
});
case AuthProvider.AZURE_SAML:
return ({
acsUrl: "Reply URL (Assertion Consumer Service URL)",
entityId: "Identifier (Entity ID)",
entryPoint: "Login URL",
entryPointPlaceholder: "https://login.microsoftonline.com/xxx/saml2",
issuer: "Azure Application ID",
issuerPlaceholder: "abc-def-ghi-jkl-mno"
});
case AuthProvider.JUMPCLOUD_SAML:
return ({
acsUrl: "ACS URL",
entityId: "SP Entity ID",
entryPoint: "IDP URL",
entryPointPlaceholder: "https://sso.jumpcloud.com/saml2/xxx",
issuer: "IdP Entity ID",
issuerPlaceholder: "xxx"
});
default:
return ({
acsUrl: "ACS URL",
entityId: "Entity ID",
entryPoint: "Entrypoint",
entryPointPlaceholder: "Enter entrypoint...",
issuer: "Issuer",
issuerPlaceholder: "Enter placeholder..."
});
}
const onSSOModalSubmit = async ({ authProvider, entryPoint, issuer, cert }: AddSSOFormData) => {
try {
if (!currentOrg) return;
if (!data) {
await createMutateAsync({
organizationId: currentOrg.id,
authProvider,
isActive: false,
entryPoint,
issuer,
cert
});
} else {
await updateMutateAsync({
organizationId: currentOrg.id,
authProvider,
isActive: false,
entryPoint,
issuer,
cert
});
}
handlePopUpClose("addSSO");
createNotification({
text: `Successfully ${!data ? "added" : "updated"} SAML SSO configuration`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${!data ? "add" : "update"} SAML SSO configuration`,
type: "error"
});
}
const authProvider = watch("authProvider");
};
return (
<Modal
isOpen={popUp?.addSSO?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addSSO", isOpen);
reset();
}}
>
<ModalContent title="Add SSO">
<form onSubmit={handleSubmit(onSSOModalSubmit)}>
<Controller
control={control}
name="authProvider"
defaultValue="okta-saml"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Type"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{ssoAuthProviders.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
const renderLabels = (authProvider: string) => {
switch (authProvider) {
case AuthProvider.OKTA_SAML:
return {
acsUrl: "Single sign-on URL",
entityId: "Audience URI (SP Entity ID)",
entryPoint: "Identity Provider Single Sign-On URL",
entryPointPlaceholder: "https://your-domain.okta.com/app/app-name/xxx/sso/saml",
issuer: "Identity Provider Issuer",
issuerPlaceholder: "http://www.okta.com/xxx"
};
case AuthProvider.AZURE_SAML:
return {
acsUrl: "Reply URL (Assertion Consumer Service URL)",
entityId: "Identifier (Entity ID)",
entryPoint: "Login URL",
entryPointPlaceholder: "https://login.microsoftonline.com/xxx/saml2",
issuer: "Azure Application ID",
issuerPlaceholder: "abc-def-ghi-jkl-mno"
};
case AuthProvider.JUMPCLOUD_SAML:
return {
acsUrl: "ACS URL",
entityId: "SP Entity ID",
entryPoint: "IDP URL",
entryPointPlaceholder: "https://sso.jumpcloud.com/saml2/xxx",
issuer: "IdP Entity ID",
issuerPlaceholder: "xxx"
};
default:
return {
acsUrl: "ACS URL",
entityId: "Entity ID",
entryPoint: "Entrypoint",
entryPointPlaceholder: "Enter entrypoint...",
issuer: "Issuer",
issuerPlaceholder: "Enter placeholder..."
};
}
};
const authProvider = watch("authProvider");
return (
<Modal
isOpen={popUp?.addSSO?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addSSO", isOpen);
reset();
}}
>
<ModalContent title="Add SSO">
<form onSubmit={handleSubmit(onSSOModalSubmit)}>
<Controller
control={control}
name="authProvider"
defaultValue="okta-saml"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Type" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{ssoAuthProviders.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{authProvider && data && (
<>
<div className="mb-4">
<h3 className="text-sm text-mineshaft-400">{renderLabels(authProvider).acsUrl}</h3>
<p className="text-md break-all text-gray-400">{`${window.origin}/api/v1/sso/saml2/${data._id}`}</p>
</div>
<div className="mb-4">
<h3 className="text-sm text-mineshaft-400">
{renderLabels(authProvider).entityId}
</h3>
<p className="text-md text-gray-400">{window.origin}</p>
</div>
<Controller
control={control}
name="entryPoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label={renderLabels(authProvider).entryPoint}
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder={renderLabels(authProvider).entryPointPlaceholder}
/>
{authProvider && data && (
<>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">{renderLabels(authProvider).acsUrl}</h3>
<p className="text-gray-400 text-md break-all">{`${window.origin}/api/v1/sso/saml2/${data.id}`}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">{renderLabels(authProvider).entityId}</h3>
<p className="text-gray-400 text-md">{window.origin}</p>
</div>
<Controller
control={control}
name="entryPoint"
render={({ field, fieldState: { error } }) => (
<FormControl
label={renderLabels(authProvider).entryPoint}
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder={renderLabels(authProvider).entryPointPlaceholder}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="issuer"
render={({ field, fieldState: { error } }) => (
<FormControl
label={renderLabels(authProvider).issuer}
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder={renderLabels(authProvider).issuerPlaceholder}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="cert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea
{...field}
placeholder="-----BEGIN CERTIFICATE----- ..."
/>
</FormControl>
)}
/>
</>
)}
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={createIsLoading || updateIsLoading}
>
{!data ? "Add" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addSSO")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
</FormControl>
)}
/>
<Controller
control={control}
name="issuer"
render={({ field, fieldState: { error } }) => (
<FormControl
label={renderLabels(authProvider).issuer}
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder={renderLabels(authProvider).issuerPlaceholder} />
</FormControl>
)}
/>
<Controller
control={control}
name="cert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
</>
)}
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={createIsLoading || updateIsLoading}
>
{!data ? "Add" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addSSO")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -45,8 +45,8 @@ export const OrgDeleteSection = () => {
};
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Danger Zone</p>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p>
<Button
isLoading={isLoading}
colorSchema="danger"
@@ -51,9 +51,9 @@ export const OrgNameChangeSection = (): JSX.Element => {
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Name</p>
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Name</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""
@@ -55,9 +55,9 @@ export const APIKeyTable = () => {
{!isLoading &&
data &&
data.length > 0 &&
data.map(({ _id, name, createdAt, expiresAt, lastUsed }) => {
data.map(({ id, name, createdAt, expiresAt, lastUsed }) => {
return (
<Tr className="h-10" key={`api-key-${_id}`}>
<Tr className="h-10" key={`api-key-${id}`}>
<Td>{name}</Td>
<Td>{format(new Date(lastUsed), "yyyy-MM-dd")}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
@@ -65,7 +65,7 @@ export const APIKeyTable = () => {
<Td>
<IconButton
onClick={async () => {
await handleDeleteAPIKeyDataClick(_id);
await handleDeleteAPIKeyDataClick(id);
}}
size="lg"
colorSchema="danger"
@@ -3,105 +3,96 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import {
useGetMyAPIKeysV2
} from "@app/hooks/api";
import { useGetMyAPIKeysV2 } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteAPIKeyV2", "apiKeyV2"]>,
data?: {
apiKeyDataId?: string;
name?: string;
}
) => void;
};
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteAPIKeyV2", "apiKeyV2"]>,
data?: {
apiKeyDataId?: string;
name?: string;
}
) => void;
};
export const APIKeyV2Table = ({
handlePopUpOpen
}: Props) => {
const { data, isLoading } = useGetMyAPIKeysV2();
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="">Name</Th>
<Th className="">Last Used</Th>
<Th className="">Created At</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="api-keys-v2" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({
id,
name,
lastUsed,
createdAt
}) => {
return (
<Tr className="h-10" key={`api-key-v2-${id}`}>
<Td>{name}</Td>
<Td>{lastUsed ? format(new Date(lastUsed), "yyyy-MM-dd") : "-"}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td className="flex justify-end">
<IconButton
onClick={async () => {
handlePopUpOpen("apiKeyV2", {
apiKeyDataId: id,
name
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
onClick={() => {
handlePopUpOpen("deleteAPIKeyV2", {
apiKeyDataId: id
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No API key v2 on file" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
export const APIKeyV2Table = ({ handlePopUpOpen }: Props) => {
const { data, isLoading } = useGetMyAPIKeysV2();
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="">Name</Th>
<Th className="">Last Used</Th>
<Th className="">Created At</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="api-keys-v2" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({ id, name, lastUsed, createdAt }) => {
return (
<Tr className="h-10" key={`api-key-v2-${id}`}>
<Td>{name}</Td>
<Td>{lastUsed ? format(new Date(lastUsed), "yyyy-MM-dd") : "-"}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td className="flex justify-end">
<IconButton
onClick={async () => {
handlePopUpOpen("apiKeyV2", {
apiKeyDataId: id,
name
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
onClick={() => {
handlePopUpOpen("deleteAPIKeyV2", {
apiKeyDataId: id
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No API key v2 on file" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
};
@@ -43,9 +43,9 @@ export const SessionsTable = () => {
{!isLoading &&
data &&
data.length > 0 &&
data.map(({ _id, createdAt, lastUsed, ip, userAgent }) => {
data.map(({ id, createdAt, lastUsed, ip, userAgent }) => {
return (
<Tr className="h-10" key={`session-${_id}`}>
<Tr className="h-10" key={`session-${id}`}>
<Td>{formatDate(createdAt)}</Td>
<Td>{formatDate(lastUsed)}</Td>
<Td>{ip}</Td>
@@ -9,7 +9,7 @@ import { useUser } from "@app/context";
import { useRenameUser } from "@app/hooks/api/users/queries";
const formSchema = yup.object({
name: yup.string().required().label("User Name"),
name: yup.string().required().label("User Name")
});
type FormData = yup.InferType<typeof formSchema>;
@@ -17,11 +17,7 @@ type FormData = yup.InferType<typeof formSchema>;
export const UserNameSection = (): JSX.Element => {
const { user } = useUser();
const { createNotification } = useNotificationContext();
const {
handleSubmit,
control,
reset
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: yupResolver(formSchema) });
const { mutateAsync, isLoading } = useRenameUser();
useEffect(() => {
@@ -32,10 +28,10 @@ export const UserNameSection = (): JSX.Element => {
const onFormSubmit = async ({ name }: FormData) => {
try {
if (!user?._id) return;
if (!user?.id) return;
if (name === "") return;
await mutateAsync({ newName: name});
await mutateAsync({ newName: name });
createNotification({
text: "Successfully renamed user",
type: "success"
@@ -50,33 +46,26 @@ export const UserNameSection = (): JSX.Element => {
};
return (
<form
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
Name
</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder={`${user?.firstName} ${user?.lastName}`} {...field} />
</FormControl>
)}
control={control}
name="name"
/>
</div>
<Button
isLoading={isLoading}
colorSchema="primary"
variant="outline_bg"
type="submit"
>
Save
</Button>
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Name</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder={`${user?.firstName} ${user?.lastName}`} {...field} />
</FormControl>
)}
control={control}
name="name"
/>
</div>
<Button isLoading={isLoading} colorSchema="primary" variant="outline_bg" type="submit">
Save
</Button>
</form>
);
};
@@ -14,10 +14,10 @@ export const AutoCapitalizationSection = () => {
const handleToggleCapitalizationToggle = async (state: boolean) => {
try {
if (!currentWorkspace?._id) return;
if (!currentWorkspace?.id) return;
await mutateAsync({
workspaceID: currentWorkspace._id,
workspaceID: currentWorkspace.id,
state
});
@@ -36,7 +36,7 @@ export const AutoCapitalizationSection = () => {
};
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
@@ -2,10 +2,7 @@ import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
} from "@app/components/v2";
import { Button, DeleteActionModal } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@@ -19,30 +16,30 @@ import { usePopUp } from "@app/hooks/usePopUp";
export const DeleteProjectSection = () => {
const router = useRouter();
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteWorkspace"
] as const);
] as const);
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const [isDeleting, setIsDeleting] = useToggle();
const deleteWorkspace = useDeleteWorkspace();
const handleDeleteWorkspaceSubmit = async () => {
setIsDeleting.on();
try {
if (!currentWorkspace?._id) return;
if (!currentWorkspace?.id) return;
await deleteWorkspace.mutateAsync({
workspaceID: currentWorkspace?._id
workspaceID: currentWorkspace?.id
});
createNotification({
text: "Successfully deleted project",
type: "success"
});
router.push(`/org/${currentOrg?._id}/overview`);
router.push(`/org/${currentOrg?.id}/overview`);
handlePopUpClose("deleteWorkspace");
} catch (err) {
console.error(err);
@@ -53,36 +50,34 @@ export const DeleteProjectSection = () => {
} finally {
setIsDeleting.off();
}
}
};
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
Danger Zone
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
{(isAllowed) => (
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
)}
</ProjectPermissionCan>
<DeleteActionModal
isOpen={popUp.deleteWorkspace.isOpen}
title="Are you sure want to delete this project?"
subTitle={`Permanently delete ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
deleteKey="confirm"
buttonText="Delete Project"
onDeleteApproved={handleDeleteWorkspaceSubmit}
/>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p>
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Workspace}>
{(isAllowed) => (
<Button
isLoading={isDeleting}
isDisabled={!isAllowed || isDeleting}
colorSchema="danger"
variant="outline_bg"
type="submit"
onClick={() => handlePopUpOpen("deleteWorkspace")}
>
{`Delete ${currentWorkspace?.name}`}
</Button>
)}
</ProjectPermissionCan>
<DeleteActionModal
isOpen={popUp.deleteWorkspace.isOpen}
title="Are you sure want to delete this project?"
subTitle={`Permanently delete ${currentWorkspace?.name} and all of its data. This action is not reversible, so please be careful.`}
onChange={(isOpen) => handlePopUpToggle("deleteWorkspace", isOpen)}
deleteKey="confirm"
buttonText="Delete Project"
onDeleteApproved={handleDeleteWorkspaceSubmit}
/>
</div>
);
};
@@ -9,9 +9,9 @@ import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@
export const E2EESection = () => {
const { currentWorkspace } = useWorkspace();
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
const { data: bot } = useGetWorkspaceBot(currentWorkspace?.id ?? "");
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
const { data: wsKey } = useGetUserWsKey(currentWorkspace?.id ?? "");
/**
* Activate bot for project by performing the following steps:
@@ -23,7 +23,7 @@ export const E2EESection = () => {
const toggleBotActivate = async () => {
let botKey;
try {
if (!currentWorkspace?._id) return;
if (!currentWorkspace?.id) return;
if (bot && wsKey) {
// case: there is a bot
@@ -56,17 +56,17 @@ export const E2EESection = () => {
};
await updateBotActiveStatus({
workspaceId: currentWorkspace._id,
workspaceId: currentWorkspace.id,
botKey,
isActive: true,
botId: bot._id
botId: bot.id
});
} else {
// bot is active -> deactivate bot
await updateBotActiveStatus({
isActive: false,
botId: bot._id,
workspaceId: currentWorkspace._id
botId: bot.id,
workspaceId: currentWorkspace.id
});
}
}
@@ -3,21 +3,15 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
} from "@app/components/v2";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateWsEnvironment } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["createEnv"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["createEnv"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["createEnv"]>, state?: boolean) => void;
popUp: UsePopUpState<["createEnv"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["createEnv"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["createEnv"]>, state?: boolean) => void;
};
const schema = yup.object({
@@ -27,106 +21,95 @@ const schema = yup.object({
export type FormData = yup.InferType<typeof schema>;
export const AddEnvironmentModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
export const AddEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const { mutateAsync, isLoading } = useCreateWsEnvironment();
const {
control,
handleSubmit,
reset
} = useForm<FormData>({
resolver: yupResolver(schema)
});
const { mutateAsync, isLoading } = useCreateWsEnvironment();
const { control, handleSubmit, reset } = useForm<FormData>({
resolver: yupResolver(schema)
});
const onFormSubmit = async ({
const onFormSubmit = async ({ environmentName, environmentSlug }: FormData) => {
try {
if (!currentWorkspace?.id) return;
await mutateAsync({
workspaceID: currentWorkspace.id,
environmentName,
environmentSlug
}: FormData) => {
try {
if (!currentWorkspace?._id) return;
});
await mutateAsync({
workspaceID: currentWorkspace._id,
environmentName,
environmentSlug
});
createNotification({
text: "Successfully created environment",
type: "success"
});
createNotification({
text: "Successfully created environment",
type: "success"
});
handlePopUpClose("createEnv");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create environment",
type: "error"
});
}
};
handlePopUpClose("createEnv");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create environment",
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.createEnv?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("createEnv", isOpen);
reset();
}}
>
<ModalContent title="Create a new environment">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="environmentName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Environment Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="environmentSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Environment Slug"
helperText="Slugs are shorthands used in cli to access environment"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isLoading}
isDisabled={isLoading}
>
Create
</Button>
return (
<Modal
isOpen={popUp?.createEnv?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("createEnv", isOpen);
reset();
}}
>
<ModalContent title="Create a new environment">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="environmentName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Environment Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="environmentSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Environment Slug"
helperText="Slugs are shorthands used in cli to access environment"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isLoading}
isDisabled={isLoading}
>
Create
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -40,10 +40,10 @@ export const EnvironmentSection = () => {
const onEnvDeleteSubmit = async (environmentSlug: string) => {
try {
if (!currentWorkspace?._id) return;
if (!currentWorkspace?.id) return;
await deleteWsEnvironment.mutateAsync({
workspaceID: currentWorkspace._id,
workspaceID: currentWorkspace.id,
environmentSlug
});
@@ -63,8 +63,8 @@ export const EnvironmentSection = () => {
};
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-8">
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-8 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Environments</p>
<div>
<ProjectPermissionCan
@@ -90,7 +90,7 @@ export const EnvironmentSection = () => {
</ProjectPermissionCan>
</div>
</div>
<p className="text-gray-400 mb-8">
<p className="mb-8 text-gray-400">
Choose which environments will show up in your dashboard like development, staging,
production
</p>
@@ -39,7 +39,7 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
const handleReorderEnv = async (shouldMoveUp: boolean, name: string, slug: string) => {
try {
if (!currentWorkspace?._id) return;
if (!currentWorkspace?.id) return;
const indexOfEnv = currentWorkspace.environments.findIndex(
(env) => env.name === name && env.slug === slug
@@ -56,7 +56,7 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
const indexToSwap = shouldMoveUp ? indexOfEnv - 1 : indexOfEnv + 1;
await reorderWsEnvironment.mutateAsync({
workspaceID: currentWorkspace._id,
workspaceID: currentWorkspace.id,
environmentSlug: slug,
environmentName: name,
otherEnvironmentSlug: currentWorkspace.environments[indexToSwap].slug,
@@ -3,21 +3,15 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
} from "@app/components/v2";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useUpdateWsEnvironment } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["updateEnv"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["updateEnv"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["updateEnv"]>, state?: boolean) => void;
popUp: UsePopUpState<["updateEnv"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["updateEnv"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["updateEnv"]>, state?: boolean) => void;
};
const schema = yup.object({
@@ -27,109 +21,98 @@ const schema = yup.object({
export type FormData = yup.InferType<typeof schema>;
export const UpdateEnvironmentModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
export const UpdateEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const { mutateAsync, isLoading } = useUpdateWsEnvironment();
const {
control,
handleSubmit,
reset
} = useForm<FormData>({
resolver: yupResolver(schema)
});
const { mutateAsync, isLoading } = useUpdateWsEnvironment();
const { control, handleSubmit, reset } = useForm<FormData>({
resolver: yupResolver(schema)
});
const oldEnvironmentSlug = (popUp?.updateEnv?.data as { slug: string })?.slug;
const onFormSubmit = async ({
const oldEnvironmentSlug = (popUp?.updateEnv?.data as { slug: string })?.slug;
const onFormSubmit = async ({ environmentName, environmentSlug }: FormData) => {
try {
if (!currentWorkspace?.id) return;
await mutateAsync({
workspaceID: currentWorkspace.id,
environmentName,
environmentSlug
}: FormData) => {
try {
if (!currentWorkspace?._id) return;
await mutateAsync({
workspaceID: currentWorkspace._id,
environmentName,
environmentSlug,
oldEnvironmentSlug
});
environmentSlug,
oldEnvironmentSlug
});
createNotification({
text: "Successfully updated environment",
type: "success"
});
handlePopUpClose("updateEnv");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update environment",
type: "error"
});
}
};
createNotification({
text: "Successfully updated environment",
type: "success"
});
return (
<Modal
isOpen={popUp?.updateEnv?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("updateEnv", isOpen);
reset();
}}
>
<ModalContent title="Update environment">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="environmentName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Environment Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="environmentSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Environment Slug"
helperText="Slugs are shorthands used in cli to access environment"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isLoading}
isDisabled={isLoading}
>
Update
</Button>
handlePopUpClose("updateEnv");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update environment",
type: "error"
});
}
};
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
return (
<Modal
isOpen={popUp?.updateEnv?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("updateEnv", isOpen);
reset();
}}
>
<ModalContent title="Update environment">
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="environmentName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Environment Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="environmentSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Environment Slug"
helperText="Slugs are shorthands used in cli to access environment"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isLoading}
isDisabled={isLoading}
>
Update
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -32,10 +32,10 @@ export const ProjectNameChangeSection = () => {
const onFormSubmit = async ({ name }: FormData) => {
try {
if (!currentWorkspace?._id) return;
if (!currentWorkspace?.id) return;
await mutateAsync({
workspaceID: currentWorkspace._id,
workspaceID: currentWorkspace.id,
newWorkspaceName: name
});
@@ -55,9 +55,9 @@ export const ProjectNameChangeSection = () => {
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Project Name</h2>
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Name</h2>
<div className="max-w-md">
<Controller
defaultValue=""
@@ -3,120 +3,108 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalClose,
ModalContent
} from "@app/components/v2";
import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateWsTag } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({
name: yup.string().required().label("Tag Name")
name: yup.string().required().label("Tag Name")
});
export type FormData = yup.InferType<typeof schema>;
type Props = {
popUp: UsePopUpState<["CreateSecretTag", "deleteTagConfirmation"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["CreateSecretTag", "deleteTagConfirmation"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["CreateSecretTag", "deleteTagConfirmation"]>, state?: boolean) => void;
popUp: UsePopUpState<["CreateSecretTag", "deleteTagConfirmation"]>;
handlePopUpClose: (
popUpName: keyof UsePopUpState<["CreateSecretTag", "deleteTagConfirmation"]>
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["CreateSecretTag", "deleteTagConfirmation"]>,
state?: boolean
) => void;
};
export const AddSecretTagModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { createNotification } = useNotificationContext();
const { currentWorkspace }= useWorkspace();
const createWsTag = useCreateWsTag();
const {
control,
reset,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema)
});
const onFormSubmit = async ({
name
}: FormData) => {
try {
if (!currentWorkspace?._id) return;
export const AddSecretTagModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props) => {
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const createWsTag = useCreateWsTag();
const {
control,
reset,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema)
});
await createWsTag.mutateAsync({
workspaceID: currentWorkspace?._id,
tagName: name,
tagSlug: name.replace(/\s+/g, " ").replace(" ", "_"),
tagColor: ""
});
const onFormSubmit = async ({ name }: FormData) => {
try {
if (!currentWorkspace?.id) return;
handlePopUpClose("CreateSecretTag");
await createWsTag.mutateAsync({
workspaceID: currentWorkspace?.id,
tagName: name,
tagSlug: name.replace(/\s+/g, " ").replace(" ", "_"),
tagColor: ""
});
createNotification({
text: "Successfully created a tag",
type: "success"
});
reset()
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create a tag",
type: "error"
});
}
handlePopUpClose("CreateSecretTag");
createNotification({
text: "Successfully created a tag",
type: "success"
});
reset();
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create a tag",
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.CreateSecretTag?.isOpen}
onOpenChange={(open) => {
handlePopUpToggle("CreateSecretTag", open);
reset();
}}
>
<ModalContent
title={`Add a tag for ${currentWorkspace?.name ?? ""}`}
subTitle="Specify your tag name, and the slug will be created automatically."
return (
<Modal
isOpen={popUp?.CreateSecretTag?.isOpen}
onOpenChange={(open) => {
handlePopUpToggle("CreateSecretTag", open);
reset();
}}
>
<ModalContent
title={`Add a tag for ${currentWorkspace?.name ?? ""}`}
subTitle="Specify your tag name, and the slug will be created automatically."
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl label="Tag Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="Type your tag name" />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
type="submit"
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Tag Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Type your tag name" />
</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>
);
}
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -33,7 +33,7 @@ type Props = {
export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useGetWsTags(currentWorkspace?._id ?? "");
const { data, isLoading } = useGetWsTags(currentWorkspace?.id ?? "");
return (
<TableContainer className="mt-4">
@@ -49,7 +49,7 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
{isLoading && <TableSkeleton columns={3} innerKey="secret-tags" />}
{!isLoading &&
data &&
data.map(({ _id, name, slug }) => (
data.map(({ id, name, slug }) => (
<Tr key={name}>
<Td>{name}</Td>
<Td>{slug}</Td>
@@ -63,7 +63,7 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
onClick={() =>
handlePopUpOpen("deleteTagConfirmation", {
name,
id: _id
id
})
}
colorSchema="danger"
@@ -36,7 +36,7 @@ export const WebhooksTab = withProjectPermission(
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || "";
const workspaceId = currentWorkspace?.id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"addWebhook",
"deleteWebhook"
@@ -182,7 +182,7 @@ export const WebhooksTab = withProjectPermission(
{!isWebhooksLoading &&
webhooks?.map(
({
_id: id,
id,
url,
environment,
secretPath,
@@ -42,7 +42,7 @@ export const AdminDashboardPage = () => {
<div>
<Tabs defaultValue={TabSections.Settings}>
<TabList>
<div className="flex flex-row border-b border-mineshaft-600 w-full">
<div className="flex w-full flex-row border-b border-mineshaft-600">
<Tab value={TabSections.Settings}>General</Tab>
</div>
</TabList>