mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 12:27:31 +00:00
Merge pull request #337 from akhilmhdh/feat/new-org-settings
Revamped the org settings page
This commit is contained in:
@@ -30,7 +30,7 @@ type Props = {
|
|||||||
type EnvironmentProps = {
|
type EnvironmentProps = {
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the component that shows the users of a certin project
|
* This is the component that shows the users of a certin project
|
||||||
@@ -91,26 +91,38 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePermissionUpdate = (index: number, val: string, membershipId: string, slug: string ) => {
|
const handlePermissionUpdate = (
|
||||||
let denials: { ability: string; environmentSlug: string; }[];
|
index: number,
|
||||||
if (val === "Read Only") {
|
val: string,
|
||||||
denials = [{
|
membershipId: string,
|
||||||
ability: "write",
|
slug: string
|
||||||
environmentSlug: slug
|
) => {
|
||||||
}];
|
let denials: { ability: string; environmentSlug: string }[];
|
||||||
} else if (val === "No Access") {
|
if (val === 'Read Only') {
|
||||||
denials = [{
|
denials = [
|
||||||
ability: "write",
|
{
|
||||||
environmentSlug: slug
|
ability: 'write',
|
||||||
}, {
|
environmentSlug: slug
|
||||||
ability: "read",
|
}
|
||||||
environmentSlug: slug
|
];
|
||||||
}];
|
} else if (val === 'No Access') {
|
||||||
} else if (val === "Add Only") {
|
denials = [
|
||||||
denials = [{
|
{
|
||||||
ability: "read",
|
ability: 'write',
|
||||||
environmentSlug: slug
|
environmentSlug: slug
|
||||||
}];
|
},
|
||||||
|
{
|
||||||
|
ability: 'read',
|
||||||
|
environmentSlug: slug
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else if (val === 'Add Only') {
|
||||||
|
denials = [
|
||||||
|
{
|
||||||
|
ability: 'read',
|
||||||
|
environmentSlug: slug
|
||||||
|
}
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
denials = [];
|
denials = [];
|
||||||
}
|
}
|
||||||
@@ -118,8 +130,12 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
if (currentPlan !== plans.professional && host === 'https://app.infisical.com') {
|
if (currentPlan !== plans.professional && host === 'https://app.infisical.com') {
|
||||||
setIsUpgradeModalOpen(true);
|
setIsUpgradeModalOpen(true);
|
||||||
} else {
|
} else {
|
||||||
const allDenials = userData[index].deniedPermissions.filter((perm: { ability: string; environmentSlug: string; }) => perm.environmentSlug !== slug).concat(denials);
|
const allDenials = userData[index].deniedPermissions
|
||||||
updateUserProjectPermission({ membershipId, denials: allDenials});
|
.filter(
|
||||||
|
(perm: { ability: string; environmentSlug: string }) => perm.environmentSlug !== slug
|
||||||
|
)
|
||||||
|
.concat(denials);
|
||||||
|
updateUserProjectPermission({ membershipId, denials: allDenials });
|
||||||
changeData([
|
changeData([
|
||||||
...userData.slice(0, index),
|
...userData.slice(0, index),
|
||||||
...[
|
...[
|
||||||
@@ -156,7 +172,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
if (subscriptions) {
|
if (subscriptions) {
|
||||||
setCurrentPlan(subscriptions.data[0].plan.product)
|
setCurrentPlan(subscriptions.data[0].plan.product);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [userData, myUser]);
|
}, [userData, myUser]);
|
||||||
@@ -186,25 +202,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
|
|
||||||
const closeUpgradeModal = () => {
|
const closeUpgradeModal = () => {
|
||||||
setIsUpgradeModalOpen(false);
|
setIsUpgradeModalOpen(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max">
|
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-700 bg-bunker">
|
||||||
<div className="absolute rounded-t-md w-full h-[3.1rem] bg-white/5" />
|
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={isUpgradeModalOpen}
|
isOpen={isUpgradeModalOpen}
|
||||||
onClose={closeUpgradeModal}
|
onClose={closeUpgradeModal}
|
||||||
text="You can change user permissions if you switch to Infisical's Professional plan."
|
text="You can change user permissions if you switch to Infisical's Professional plan."
|
||||||
/>
|
/>
|
||||||
<table className="w-full my-0.5">
|
<table className="my-0.5 w-full">
|
||||||
<thead className="text-gray-400 text-xs font-light">
|
<thead className="text-xs font-light text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left pl-4 py-3.5">NAME</th>
|
<th className="py-3.5 pl-4 text-left">NAME</th>
|
||||||
<th className="text-left pl-4 py-3.5">EMAIL</th>
|
<th className="py-3.5 pl-4 text-left">EMAIL</th>
|
||||||
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
|
<th className="py-3.5 pl-6 pr-10 text-left">ROLE</th>
|
||||||
{workspaceEnvs.map(env => (
|
{workspaceEnvs.map((env) => (
|
||||||
<th key={guidGenerator()} className="text-left pl-2 py-1 max-w-min break-normal">
|
<th key={guidGenerator()} className="max-w-min break-normal py-1 pl-2 text-left">
|
||||||
<span>{env.slug.toUpperCase()}<br/></span>
|
<span>
|
||||||
|
{env.slug.toUpperCase()}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
{/* <span>PERMISSION</span> */}
|
{/* <span>PERMISSION</span> */}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
@@ -227,28 +246,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
)
|
)
|
||||||
.map((row, index) => (
|
.map((row, index) => (
|
||||||
<tr key={guidGenerator()} className="bg-bunker-600 text-sm hover:bg-bunker-500">
|
<tr key={guidGenerator()} className="bg-bunker-600 text-sm hover:bg-bunker-500">
|
||||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300">
|
||||||
{row.firstName} {row.lastName}
|
{row.firstName} {row.lastName}
|
||||||
</td>
|
</td>
|
||||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300">
|
||||||
{row.email}
|
{row.email}
|
||||||
</td>
|
</td>
|
||||||
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="border-t border-mineshaft-700 py-2 pl-6 pr-10 text-gray-300">
|
||||||
<div className="justify-start h-full flex flex-row items-center">
|
<div className="flex h-full flex-row items-center justify-start">
|
||||||
<Select
|
<Select
|
||||||
className="w-36 bg-mineshaft-700"
|
className="w-36 bg-mineshaft-700"
|
||||||
dropdownContainerClassName="bg-mineshaft-700"
|
dropdownContainerClassName="bg-mineshaft-700"
|
||||||
// open={isOpen}
|
// open={isOpen}
|
||||||
onValueChange={(e) => handleRoleUpdate(index, e)}
|
onValueChange={(e) => handleRoleUpdate(index, e)}
|
||||||
value={row.role}
|
value={row.role}
|
||||||
disabled={myRole !== 'admin' || myUser === row.email}
|
isDisabled={myRole !== 'admin' || myUser === row.email}
|
||||||
// onOpenChange={(open) => setIsOpen(open)}
|
// onOpenChange={(open) => setIsOpen(open)}
|
||||||
>
|
>
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
<SelectItem value="member">Member</SelectItem>
|
<SelectItem value="member">Member</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
{row.status === 'completed' && myUser !== row.email && (
|
{row.status === 'completed' && myUser !== row.email && (
|
||||||
<div className="border border-mineshaft-700 rounded-md bg-white/5 hover:bg-primary text-white hover:text-black duration-200">
|
<div className="rounded-md border border-mineshaft-700 bg-white/5 text-white duration-200 hover:bg-primary hover:text-black">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
|
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
@@ -259,43 +278,106 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{workspaceEnvs.map((env) => <td key={guidGenerator()} className="pl-2 py-2 border-mineshaft-700 border-t text-gray-300">
|
{workspaceEnvs.map((env) => (
|
||||||
<Select
|
<td
|
||||||
className="w-16 bg-mineshaft-700"
|
key={guidGenerator()}
|
||||||
dropdownContainerClassName="bg-mineshaft-700"
|
className="border-t border-mineshaft-700 py-2 pl-2 text-gray-300"
|
||||||
position="item-aligned"
|
|
||||||
// open={isOpen}
|
|
||||||
onValueChange={(val) => handlePermissionUpdate(index, val, row.membershipId, env.slug)}
|
|
||||||
value={
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
|
||||||
(row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
|
|
||||||
? "No Access"
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
|
||||||
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Read Only"
|
|
||||||
: !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Add Only" : "Read & Write")
|
|
||||||
}
|
|
||||||
icon={
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
|
||||||
(row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
|
|
||||||
? faEyeSlash
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
|
||||||
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faEye
|
|
||||||
: !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faPlus : faPenToSquare)
|
|
||||||
}
|
|
||||||
disabled={myRole !== 'admin'}
|
|
||||||
// onOpenChange={(open) => setIsOpen(open)}
|
|
||||||
>
|
>
|
||||||
<SelectItem value="No Access" customIcon={faEyeSlash}>No Access</SelectItem>
|
<Select
|
||||||
<SelectItem value="Read Only" customIcon={faEye}>Read Only</SelectItem>
|
className="w-16 bg-mineshaft-700"
|
||||||
<SelectItem value="Add Only" customIcon={faPlus}>Add Only</SelectItem>
|
dropdownContainerClassName="bg-mineshaft-700"
|
||||||
<SelectItem value="Read & Write" customIcon={faPenToSquare}>Read & Write</SelectItem>
|
position="item-aligned"
|
||||||
</Select>
|
// open={isOpen}
|
||||||
</td>)}
|
onValueChange={(val) =>
|
||||||
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
handlePermissionUpdate(index, val, row.membershipId, env.slug)
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('write') &&
|
||||||
|
row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('read')
|
||||||
|
? 'No Access'
|
||||||
|
: // eslint-disable-next-line no-nested-ternary
|
||||||
|
row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('write') &&
|
||||||
|
!row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('read')
|
||||||
|
? 'Read Only'
|
||||||
|
: !row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('write') &&
|
||||||
|
row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('read')
|
||||||
|
? 'Add Only'
|
||||||
|
: 'Read & Write'
|
||||||
|
}
|
||||||
|
icon={
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('write') &&
|
||||||
|
row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('read')
|
||||||
|
? faEyeSlash
|
||||||
|
: // eslint-disable-next-line no-nested-ternary
|
||||||
|
row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('write') &&
|
||||||
|
!row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('read')
|
||||||
|
? faEye
|
||||||
|
: !row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('write') &&
|
||||||
|
row.deniedPermissions
|
||||||
|
.filter((perm: any) => perm.environmentSlug === env.slug)
|
||||||
|
.map((perm: { ability: string }) => perm.ability)
|
||||||
|
.includes('read')
|
||||||
|
? faPlus
|
||||||
|
: faPenToSquare
|
||||||
|
}
|
||||||
|
isDisabled={myRole !== 'admin'}
|
||||||
|
// onOpenChange={(open) => setIsOpen(open)}
|
||||||
|
>
|
||||||
|
<SelectItem value="No Access" customIcon={faEyeSlash}>
|
||||||
|
No Access
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="Read Only" customIcon={faEye}>
|
||||||
|
Read Only
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="Add Only" customIcon={faPlus}>
|
||||||
|
Add Only
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="Read & Write" customIcon={faPenToSquare}>
|
||||||
|
Read & Write
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="border-0.5 flex flex-row justify-end border-t border-mineshaft-700 py-2 pl-8 pr-8">
|
||||||
{myUser !== row.email &&
|
{myUser !== row.email &&
|
||||||
// row.role !== "admin" &&
|
// row.role !== "admin" &&
|
||||||
myRole !== 'member' ? (
|
myRole !== 'member' ? (
|
||||||
<div className="opacity-50 hover:opacity-100 flex items-center mt-0.5">
|
<div className="mt-0.5 flex items-center opacity-50 hover:opacity-100">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
||||||
color="red"
|
color="red"
|
||||||
@@ -304,7 +386,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-9 h-9" />
|
<div className="h-9 w-9" />
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ import { twMerge } from 'tailwind-merge';
|
|||||||
|
|
||||||
export type CardTitleProps = {
|
export type CardTitleProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
subTitle?: string;
|
subTitle?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
||||||
<div className={twMerge('px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600', className)}>
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
{subTitle && <p className="pt-0.5 text-sm font-normal text-gray-400">{subTitle}</p>}
|
{subTitle && <p className="pt-2 text-sm font-normal text-gray-400">{subTitle}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useToggle } from '@app/hooks';
|
|||||||
import { Button } from '../Button';
|
import { Button } from '../Button';
|
||||||
import { FormControl } from '../FormControl';
|
import { FormControl } from '../FormControl';
|
||||||
import { Input } from '../Input';
|
import { Input } from '../Input';
|
||||||
import { Modal, ModalContent } from '../Modal';
|
import { Modal, ModalClose, ModalContent } from '../Modal';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
@@ -64,9 +64,11 @@ export const DeleteActionModal = ({
|
|||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
|
<ModalClose asChild>
|
||||||
Cancel
|
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
|
||||||
</Button>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>{' '}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
|||||||
@@ -84,19 +84,19 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
|
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
|
||||||
{leftIcon && <span className="absolute left-0 ml-2">{leftIcon}</span>}
|
{leftIcon && <span className="absolute left-0 ml-3">{leftIcon}</span>}
|
||||||
<input
|
<input
|
||||||
{...props}
|
{...props}
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
leftIcon ? 'pl-9' : 'pl-2.5',
|
leftIcon ? 'pl-10' : 'pl-2.5',
|
||||||
rightIcon ? 'pr-9' : 'pr-2.5',
|
rightIcon ? 'pr-10' : 'pr-2.5',
|
||||||
inputVariants({ className, isError, size, isRounded, variant })
|
inputVariants({ className, isError, size, isRounded, variant })
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{rightIcon && <span className="absolute right-0 mr-2">{rightIcon}</span>}
|
{rightIcon && <span className="absolute right-0 mr-3">{rightIcon}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { IconButton } from '../IconButton';
|
|||||||
|
|
||||||
export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
subTitle?: string;
|
subTitle?: ReactNode;
|
||||||
footerContent?: ReactNode;
|
footerContent?: ReactNode;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,18 +14,28 @@ type Props = {
|
|||||||
dropdownContainerClassName?: string;
|
dropdownContainerClassName?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
position?: 'item-aligned' | 'popper';
|
position?: 'item-aligned' | 'popper';
|
||||||
|
isDisabled?: boolean;
|
||||||
icon?: IconProp;
|
icon?: IconProp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SelectProps = SelectPrimitive.SelectProps & Props;
|
export type SelectProps = Omit<SelectPrimitive.SelectProps, 'disabled'> & Props;
|
||||||
|
|
||||||
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||||
(
|
(
|
||||||
{ children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
|
{
|
||||||
|
children,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
isLoading,
|
||||||
|
isDisabled,
|
||||||
|
dropdownContainerClassName,
|
||||||
|
position,
|
||||||
|
...props
|
||||||
|
},
|
||||||
ref
|
ref
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Root {...props}>
|
<SelectPrimitive.Root {...props} disabled={isDisabled}>
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@@ -34,10 +44,10 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SelectPrimitive.Value placeholder={placeholder}>
|
<SelectPrimitive.Value placeholder={placeholder}>
|
||||||
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
|
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
|
||||||
</SelectPrimitive.Value>
|
</SelectPrimitive.Value>
|
||||||
{!props.disabled && (
|
{!isDisabled && (
|
||||||
<SelectPrimitive.Icon className="ml-3">
|
<SelectPrimitive.Icon className="ml-3">
|
||||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
@@ -46,7 +56,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'relative top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
|
'relative top-1 z-[100] overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md',
|
||||||
dropdownContainerClassName
|
dropdownContainerClassName
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
@@ -89,11 +99,12 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`relative flex cursor-pointer
|
`relative mb-0.5 flex
|
||||||
select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm
|
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
|
||||||
outline-none transition-all hover:bg-mineshaft-500`,
|
outline-none transition-all hover:bg-mineshaft-500`,
|
||||||
isSelected && 'bg-primary',
|
isSelected && 'bg-primary',
|
||||||
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
|
isDisabled &&
|
||||||
|
'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { cva, VariantProps } from 'cva';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & VariantProps<typeof tagVariants>;
|
||||||
|
|
||||||
|
const tagVariants = cva('inline-flex whitespace-nowrap text-sm rounded-md mx-1 text-bunker-200 ', {
|
||||||
|
variants: {
|
||||||
|
colorSchema: {
|
||||||
|
gray: 'bg-mineshaft-500',
|
||||||
|
red: 'bg-red/80 text-bunker-100'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
sm: 'px-2 py-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Tag = ({ children, className, colorSchema = 'gray', size = 'sm' }: Props) => (
|
||||||
|
<div className={twMerge(tagVariants({ colorSchema, className, size }))}>{children}</div>
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Tag } from './Tag';
|
||||||
@@ -12,5 +12,6 @@ export * from './Select';
|
|||||||
export * from './Spinner';
|
export * from './Spinner';
|
||||||
export * from './Switch';
|
export * from './Switch';
|
||||||
export * from './Table';
|
export * from './Table';
|
||||||
|
export * from './Tag';
|
||||||
export * from './TextArea';
|
export * from './TextArea';
|
||||||
export * from './UpgradePlanModal';
|
export * from './UpgradePlanModal';
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export {
|
||||||
|
useAddIncidentContact,
|
||||||
|
useDeleteIncidentContact,
|
||||||
|
useGetOrgIncidentContact
|
||||||
|
} from './queries';
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { apiRequest } from '@app/config/request';
|
||||||
|
|
||||||
|
import { AddIncidentContactDTO, DeleteIncidentContactDTO, IncidentContact } from './types';
|
||||||
|
|
||||||
|
const incidentContactKeys = {
|
||||||
|
getAllContact: (orgId: string) => ['org-incident-contacts', { orgId }] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOrgIncidentContacts = async (orgId: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ incidentContactsOrg: IncidentContact[] }>(
|
||||||
|
`/api/v1/organization/${orgId}/incidentContactOrg`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.incidentContactsOrg;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetOrgIncidentContact = (orgId: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: incidentContactKeys.getAllContact(orgId),
|
||||||
|
queryFn: () => fetchOrgIncidentContacts(orgId),
|
||||||
|
enabled: Boolean(orgId)
|
||||||
|
});
|
||||||
|
|
||||||
|
// mutation
|
||||||
|
export const useAddIncidentContact = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, AddIncidentContactDTO>({
|
||||||
|
mutationFn: async ({ orgId, email }) => {
|
||||||
|
const { data } = await apiRequest.post(`/api/v1/organization/${orgId}/incidentContactOrg`, {
|
||||||
|
email
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId }) => {
|
||||||
|
queryClient.invalidateQueries(incidentContactKeys.getAllContact(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteIncidentContact = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, DeleteIncidentContactDTO>({
|
||||||
|
mutationFn: async ({ orgId, email }) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v1/organization/${orgId}/incidentContactOrg`, {
|
||||||
|
data: { email }
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId }) => {
|
||||||
|
queryClient.invalidateQueries(incidentContactKeys.getAllContact(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export type IncidentContact = {
|
||||||
|
_id: string;
|
||||||
|
email: string;
|
||||||
|
organization: string;
|
||||||
|
__v: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteIncidentContactDTO = {
|
||||||
|
orgId: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddIncidentContactDTO = {
|
||||||
|
orgId: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
export * from './incidentContacts';
|
||||||
export * from './keys';
|
export * from './keys';
|
||||||
export * from './organization';
|
export * from './organization';
|
||||||
export * from './serviceTokens';
|
export * from './serviceTokens';
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { useGetOrganization } from './queries';
|
export { useGetOrganization, useRenameOrg } from './queries';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { apiRequest } from '@app/config/request';
|
import { apiRequest } from '@app/config/request';
|
||||||
|
|
||||||
import { Organization } from './types';
|
import { Organization, RenameOrgDTO } from './types';
|
||||||
|
|
||||||
const organizationKeys = {
|
const organizationKeys = {
|
||||||
getUserOrganization: ['organization'] as const
|
getUserOrganization: ['organization'] as const
|
||||||
@@ -16,3 +16,16 @@ const fetchUserOrganization = async () => {
|
|||||||
|
|
||||||
export const useGetOrganization = () =>
|
export const useGetOrganization = () =>
|
||||||
useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization });
|
useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization });
|
||||||
|
|
||||||
|
// mutation
|
||||||
|
export const useRenameOrg = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, RenameOrgDTO>({
|
||||||
|
mutationFn: ({ newOrgName, orgId }) =>
|
||||||
|
apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(organizationKeys.getUserOrganization);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,3 +4,8 @@ export type Organization = {
|
|||||||
createAt: string;
|
createAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RenameOrgDTO = {
|
||||||
|
orgId: string;
|
||||||
|
newOrgName: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
export type { GetAuthTokenAPI } from './auth/types';
|
export type { GetAuthTokenAPI } from './auth/types';
|
||||||
|
export type { IncidentContact } from './incidentContacts/types';
|
||||||
export type { UserWsKeyPair } from './keys/types';
|
export type { UserWsKeyPair } from './keys/types';
|
||||||
export type { Organization } from './organization/types';
|
export type { Organization } from './organization/types';
|
||||||
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
|
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
|
||||||
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
|
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
|
||||||
export type { User } from './users/types';
|
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types';
|
||||||
export type {
|
export type {
|
||||||
CreateEnvironmentDTO,
|
CreateEnvironmentDTO,
|
||||||
|
CreateWorkspaceDTO,
|
||||||
DeleteEnvironmentDTO,
|
DeleteEnvironmentDTO,
|
||||||
DeleteWorkspaceDTO,
|
DeleteWorkspaceDTO,
|
||||||
RenameWorkspaceDTO,
|
RenameWorkspaceDTO,
|
||||||
|
ToggleAutoCapitalizationDTO,
|
||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceEnv,
|
WorkspaceEnv,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
export {
|
export {
|
||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
|
useAddUserToOrg,
|
||||||
useAddUserToWs,
|
useAddUserToWs,
|
||||||
|
useDeleteOrgMembership,
|
||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetUser,
|
useGetUser,
|
||||||
useLogoutUser
|
useLogoutUser,
|
||||||
|
useUpdateOrgUserRole
|
||||||
} from './queries';
|
} from './queries';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
@@ -8,7 +8,15 @@ import { apiRequest } from '@app/config/request';
|
|||||||
import { setAuthToken } from '@app/reactQuery';
|
import { setAuthToken } from '@app/reactQuery';
|
||||||
|
|
||||||
import { useUploadWsKey } from '../keys/queries';
|
import { useUploadWsKey } from '../keys/queries';
|
||||||
import { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './types';
|
import {
|
||||||
|
AddUserToOrgDTO,
|
||||||
|
AddUserToWsDTO,
|
||||||
|
AddUserToWsRes,
|
||||||
|
DeletOrgMembershipDTO,
|
||||||
|
OrgUser,
|
||||||
|
UpdateOrgUserRoleDTO,
|
||||||
|
User
|
||||||
|
} from './types';
|
||||||
|
|
||||||
const userKeys = {
|
const userKeys = {
|
||||||
getUser: ['user'] as const,
|
getUser: ['user'] as const,
|
||||||
@@ -32,7 +40,11 @@ export const fetchOrgUsers = async (orgId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useGetOrgUsers = (orgId: string) =>
|
export const useGetOrgUsers = (orgId: string) =>
|
||||||
useQuery(userKeys.getOrgUsers(orgId), () => fetchOrgUsers(orgId));
|
useQuery({
|
||||||
|
queryKey: userKeys.getOrgUsers(orgId),
|
||||||
|
queryFn: () => fetchOrgUsers(orgId),
|
||||||
|
enabled: Boolean(orgId)
|
||||||
|
});
|
||||||
|
|
||||||
// mutation
|
// mutation
|
||||||
export const useAddUserToWs = () => {
|
export const useAddUserToWs = () => {
|
||||||
@@ -69,6 +81,47 @@ export const useAddUserToWs = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAddUserToOrg = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, AddUserToOrgDTO>({
|
||||||
|
mutationFn: (dto) => apiRequest.post(`/api/v1/invite-org/signup`, dto),
|
||||||
|
onSuccess: (_, { organizationId }) => {
|
||||||
|
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteOrgMembership = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, DeletOrgMembershipDTO>({
|
||||||
|
mutationFn: ({ membershipId, orgId }) =>
|
||||||
|
apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`),
|
||||||
|
onSuccess: (_, { orgId }) => {
|
||||||
|
queryClient.invalidateQueries(userKeys.getOrgUsers(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateOrgUserRole = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, UpdateOrgUserRoleDTO>({
|
||||||
|
mutationFn: ({ organizationId, membershipId, role }) =>
|
||||||
|
apiRequest.patch(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, {
|
||||||
|
role
|
||||||
|
}),
|
||||||
|
onSuccess: (_, { organizationId }) => {
|
||||||
|
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
||||||
|
},
|
||||||
|
// to remove old states
|
||||||
|
onError: (_, { organizationId }) => {
|
||||||
|
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useLogoutUser = () =>
|
export const useLogoutUser = () =>
|
||||||
useMutation({
|
useMutation({
|
||||||
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
|
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export type OrgUser = {
|
|||||||
inviteEmail: string;
|
inviteEmail: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
role: 'owner' | 'admin' | 'member';
|
role: 'owner' | 'admin' | 'member';
|
||||||
status: 'invited' | 'accepted';
|
status: 'invited' | 'accepted' | 'verified' | 'completed';
|
||||||
deniedPermissions: any[];
|
deniedPermissions: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,3 +45,19 @@ export type AddUserToWsRes = {
|
|||||||
invitee: OrgUser['user'];
|
invitee: OrgUser['user'];
|
||||||
latestKey: UserWsKeyPair;
|
latestKey: UserWsKeyPair;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateOrgUserRoleDTO = {
|
||||||
|
organizationId: string;
|
||||||
|
membershipId: string;
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeletOrgMembershipDTO = {
|
||||||
|
membershipId: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddUserToOrgDTO = {
|
||||||
|
inviteeEmail: string;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ export {
|
|||||||
useCreateWsEnvironment,
|
useCreateWsEnvironment,
|
||||||
useDeleteWorkspace,
|
useDeleteWorkspace,
|
||||||
useDeleteWsEnvironment,
|
useDeleteWsEnvironment,
|
||||||
|
useGetUserWorkspaceMemberships,
|
||||||
useGetUserWorkspaces,
|
useGetUserWorkspaces,
|
||||||
useGetWorkspaceById,
|
useGetWorkspaceById,
|
||||||
useRenameWorkspace,
|
useRenameWorkspace,
|
||||||
useToggleAutoCapitalization,
|
useToggleAutoCapitalization,
|
||||||
useUpdateWsEnvironment} from './queries';
|
useUpdateWsEnvironment} from './queries';
|
||||||
|
|
||||||
@@ -13,16 +13,18 @@ import {
|
|||||||
Workspace
|
Workspace
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
|
|
||||||
const workspaceKeys = {
|
const workspaceKeys = {
|
||||||
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
|
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
|
||||||
|
getWorkspaceMemberships: (orgId: string) => [{ orgId }, 'workspace-memberships'],
|
||||||
getAllUserWorkspace: ['workspaces'] as const
|
getAllUserWorkspace: ['workspaces'] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||||
const { data } = await apiRequest.get<{ workspace: Workspace }>(`/api/v1/workspace/${workspaceId}`);
|
const { data } = await apiRequest.get<{ workspace: Workspace }>(
|
||||||
return data.workspace;
|
`/api/v1/workspace/${workspaceId}`
|
||||||
}
|
);
|
||||||
|
return data.workspace;
|
||||||
|
};
|
||||||
|
|
||||||
const fetchUserWorkspaces = async () => {
|
const fetchUserWorkspaces = async () => {
|
||||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
|
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
|
||||||
@@ -40,6 +42,21 @@ export const useGetWorkspaceById = (workspaceId: string) => {
|
|||||||
export const useGetUserWorkspaces = () =>
|
export const useGetUserWorkspaces = () =>
|
||||||
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
|
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
|
||||||
|
|
||||||
|
const fetchUserWorkspaceMemberships = async (orgId: string) => {
|
||||||
|
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
|
||||||
|
`/api/v1/organization/${orgId}/workspace-memberships`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// to get all userids in an org with the workspace they are part of
|
||||||
|
export const useGetUserWorkspaceMemberships = (orgId: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: workspaceKeys.getWorkspaceMemberships(orgId),
|
||||||
|
queryFn: () => fetchUserWorkspaceMemberships(orgId),
|
||||||
|
enabled: Boolean(orgId)
|
||||||
|
});
|
||||||
|
|
||||||
// mutation
|
// mutation
|
||||||
export const useCreateWorkspace = () => {
|
export const useCreateWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -70,7 +87,9 @@ export const useToggleAutoCapitalization = () => {
|
|||||||
|
|
||||||
return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({
|
return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({
|
||||||
mutationFn: ({ workspaceID, state }) =>
|
mutationFn: ({ workspaceID, state }) =>
|
||||||
apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, { autoCapitalization: state }),
|
apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, {
|
||||||
|
autoCapitalization: state
|
||||||
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,374 +1,21 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { faCheck, faMagnifyingGlass, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { plans } from 'public/data/frequentConstants';
|
|
||||||
|
|
||||||
import Button from '@app/components/basic/buttons/Button';
|
|
||||||
import AddIncidentContactDialog from '@app/components/basic/dialog/AddIncidentContactDialog';
|
|
||||||
import AddUserDialog from '@app/components/basic/dialog/AddUserDialog';
|
|
||||||
import UpgradePlanModal from '@app/components/basic/dialog/UpgradePlan';
|
|
||||||
import InputField from '@app/components/basic/InputField';
|
|
||||||
import UserTable from '@app/components/basic/table/UserTable';
|
|
||||||
import NavHeader from '@app/components/navigation/NavHeader';
|
|
||||||
import guidGenerator from '@app/components/utilities/randomId';
|
|
||||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||||
|
import { OrgSettingsPage } from '@app/views/Settings/OrgSettingsPage';
|
||||||
import addUserToOrg from '../../api/organization/addUserToOrg';
|
|
||||||
import deleteIncidentContact from '../../api/organization/deleteIncidentContact';
|
|
||||||
import getIncidentContacts from '../../api/organization/getIncidentContacts';
|
|
||||||
import getOrganization from '../../api/organization/GetOrg';
|
|
||||||
import getOrganizationSubscriptions from '../../api/organization/GetOrgSubscription';
|
|
||||||
import getOrganizationUsers from '../../api/organization/GetOrgUsers';
|
|
||||||
import renameOrg from '../../api/organization/renameOrg';
|
|
||||||
import getUser from '../../api/user/getUser';
|
|
||||||
import deleteWorkspace from '../../api/workspace/deleteWorkspace';
|
|
||||||
import getWorkspaces from '../../api/workspace/getWorkspaces';
|
|
||||||
|
|
||||||
export default function SettingsOrg() {
|
export default function SettingsOrg() {
|
||||||
const [buttonReady, setButtonReady] = useState(false);
|
|
||||||
const router = useRouter();
|
|
||||||
const host = window.location.origin;
|
|
||||||
const [orgName, setOrgName] = useState('');
|
|
||||||
const [emailUser, setEmailUser] = useState('');
|
|
||||||
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
|
|
||||||
const [searchUsers, setSearchUsers] = useState('');
|
|
||||||
const [isAddIncidentContactOpen, setIsAddIncidentContactOpen] = useState(false);
|
|
||||||
const [isAddUserOpen, setIsAddUserOpen] = useState(router.asPath.split('?')[1] === 'invite');
|
|
||||||
const [incidentContacts, setIncidentContacts] = useState<string[]>([]);
|
|
||||||
const [searchIncidentContact, setSearchIncidentContact] = useState('');
|
|
||||||
const [userList, setUserList] = useState<any[]>([]);
|
|
||||||
const [personalEmail, setPersonalEmail] = useState('');
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [currentPlan, setCurrentPlan] = useState('');
|
|
||||||
|
|
||||||
const workspaceId = router.query.id as string;
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const orgId = localStorage.getItem('orgData.id') as string;
|
|
||||||
const org = await getOrganization({
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
|
|
||||||
setOrgName(org.name);
|
|
||||||
const incidentContactsData = await getIncidentContacts(
|
|
||||||
localStorage.getItem('orgData.id') as string
|
|
||||||
);
|
|
||||||
|
|
||||||
setIncidentContacts(incidentContactsData?.map((contact) => contact.email));
|
|
||||||
|
|
||||||
const user = await getUser();
|
|
||||||
setPersonalEmail(user.email);
|
|
||||||
|
|
||||||
const orgUsers = await getOrganizationUsers({
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
|
|
||||||
setUserList(
|
|
||||||
orgUsers.map((orgUser) => ({
|
|
||||||
key: guidGenerator(),
|
|
||||||
firstName: orgUser.user?.firstName,
|
|
||||||
lastName: orgUser.user?.lastName,
|
|
||||||
email: orgUser.user?.email == null ? orgUser.inviteEmail : orgUser.user?.email,
|
|
||||||
role: orgUser?.role,
|
|
||||||
status: orgUser?.status,
|
|
||||||
userId: orgUser.user?._id,
|
|
||||||
membershipId: orgUser._id,
|
|
||||||
publicKey: orgUser.user?.publicKey
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
const subscriptions = await getOrganizationSubscriptions({
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
if (subscriptions) {
|
|
||||||
setCurrentPlan(subscriptions.data[0].plan.product);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const modifyOrgName = (newName: string) => {
|
|
||||||
setButtonReady(true);
|
|
||||||
setOrgName(newName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitChanges = (newOrgName: string) => {
|
|
||||||
renameOrg(localStorage.getItem('orgData.id') as string, newOrgName);
|
|
||||||
setButtonReady(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeAddUserModal = () => {
|
|
||||||
setIsAddUserOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeAddIncidentContactModal = () => {
|
|
||||||
setIsAddIncidentContactOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openAddUserModal = () => {
|
|
||||||
setIsAddUserOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openAddIncidentContactModal = () => {
|
|
||||||
setIsAddIncidentContactOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitAddUserModal = async (newUserEmail: string) => {
|
|
||||||
await addUserToOrg(newUserEmail, localStorage.getItem('orgData.id') as string);
|
|
||||||
setEmail('');
|
|
||||||
setIsAddUserOpen(false);
|
|
||||||
router.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteIncidentContactFully = (incidentContact: string) => {
|
|
||||||
setIncidentContacts(incidentContacts.filter((contact) => contact !== incidentContact));
|
|
||||||
deleteIncidentContact(localStorage.getItem('orgData.id') as string, incidentContact);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function deleted a workspace.
|
|
||||||
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
|
|
||||||
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
|
|
||||||
* It then deletes the workspace and forwards the user to another aviable workspace.
|
|
||||||
*/
|
|
||||||
const executeDeletingWorkspace = async () => {
|
|
||||||
const userWorkspaces = await getWorkspaces();
|
|
||||||
|
|
||||||
if (userWorkspaces.length > 1) {
|
|
||||||
if (
|
|
||||||
userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
|
|
||||||
workspaceToBeDeletedName
|
|
||||||
) {
|
|
||||||
await deleteWorkspace(workspaceId);
|
|
||||||
const ws = await getWorkspaces();
|
|
||||||
router.push(`/dashboard/${ws[0]._id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t('common:head-title', { title: t('settings-org:title') })}</title>
|
<title>{t('common:head-title', { title: t('settings-org:title') })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel="icon" href="/infisical.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-row">
|
<OrgSettingsPage />
|
||||||
<div className="w-full max-h-screen pb-2">
|
</>
|
||||||
<NavHeader pageName={t('settings-org:title')} />
|
|
||||||
<AddIncidentContactDialog
|
|
||||||
isOpen={isAddIncidentContactOpen}
|
|
||||||
closeModal={closeAddIncidentContactModal}
|
|
||||||
incidentContacts={incidentContacts}
|
|
||||||
setIncidentContacts={setIncidentContacts}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
|
|
||||||
<div className="flex flex-col justify-start items-start text-3xl">
|
|
||||||
<p className="font-semibold mr-4 text-gray-200">{t('settings-org:title')}</p>
|
|
||||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
|
||||||
{t('settings-org:description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-8xl">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="min-w-md mt-2 flex flex-col items-end pb-4">
|
|
||||||
<div className="bg-white/5 rounded-md px-6 py-4 flex flex-col items-start w-full mb-6">
|
|
||||||
<div className="max-h-28 w-full max-w-md mr-auto">
|
|
||||||
<p className="font-semibold mr-4 text-gray-200 text-xl mb-2">
|
|
||||||
{t('common:display-name')}
|
|
||||||
</p>
|
|
||||||
<InputField
|
|
||||||
label=""
|
|
||||||
// label="Organization Name"
|
|
||||||
onChangeHandler={modifyOrgName}
|
|
||||||
type="varName"
|
|
||||||
value={orgName}
|
|
||||||
placeholder=""
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-start w-full">
|
|
||||||
<div className="flex justify-start max-w-sm mt-4 mb-2">
|
|
||||||
<Button
|
|
||||||
text={t('common:save-changes') as string}
|
|
||||||
onButtonPressed={() => submitChanges(orgName)}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
active={buttonReady}
|
|
||||||
iconDisabled={faCheck}
|
|
||||||
textDisabled={t('common:saved') as string}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start w-full mb-6">
|
|
||||||
<p className="font-semibold mr-4 text-white text-xl">
|
|
||||||
{t('section-members:org-members')}
|
|
||||||
</p>
|
|
||||||
<p className="mr-4 text-gray-400 mt-2 mb-2">
|
|
||||||
{t('section-members:org-members-description')}
|
|
||||||
</p>
|
|
||||||
<AddUserDialog
|
|
||||||
isOpen={isAddUserOpen && (userList.length < 5 || currentPlan !== plans.starter || host !== 'https://app.infisical.com')}
|
|
||||||
closeModal={closeAddUserModal}
|
|
||||||
submitModal={submitAddUserModal}
|
|
||||||
email={emailUser}
|
|
||||||
setEmail={setEmailUser}
|
|
||||||
orgName={orgName}
|
|
||||||
/>
|
|
||||||
<UpgradePlanModal
|
|
||||||
isOpen={isAddUserOpen && userList.length >= 5 && currentPlan === plans.starter && host === 'https://app.infisical.com'}
|
|
||||||
onClose={closeAddUserModal}
|
|
||||||
text="You can add more members if you switch to Infisical's Team plan."
|
|
||||||
/>
|
|
||||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
|
||||||
<div className="pb-1 w-full flex flex-row items-start">
|
|
||||||
<div className="h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex-row ">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
|
|
||||||
icon={faMagnifyingGlass}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none"
|
|
||||||
value={searchUsers}
|
|
||||||
onChange={(e) => setSearchUsers(e.target.value)}
|
|
||||||
placeholder={t('section-members:search-members') as string}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 ml-2 min-w-max flex flex-row items-start justify-start">
|
|
||||||
<Button
|
|
||||||
text={t('section-members:add-member') as string}
|
|
||||||
onButtonPressed={openAddUserModal}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
icon={faPlus}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{userList && (
|
|
||||||
<div className="overflow-y-auto w-full">
|
|
||||||
<UserTable
|
|
||||||
userData={userList}
|
|
||||||
changeData={setUserList}
|
|
||||||
myUser={personalEmail}
|
|
||||||
filter={searchUsers.toLowerCase()}
|
|
||||||
resendInvite={submitAddUserModal}
|
|
||||||
isOrg
|
|
||||||
// onClick={openDeleteModal}
|
|
||||||
// deleteUser={deleteMembership}
|
|
||||||
// setUserIdToBeDeleted={setUserIdToBeDeleted}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-6 flex flex-col items-start w-full mb-6 mt-4">
|
|
||||||
<div className="flex flex-row max-w-5xl justify-between items-center w-full">
|
|
||||||
<div className="flex flex-col justify-between w-full max-w-3xl">
|
|
||||||
<p className="text-xl font-semibold mb-3 min-w-max">
|
|
||||||
{t('section-incident:incident-contacts')}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 mb-2 min-w-max">
|
|
||||||
{t('section-incident:incident-contacts-description')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 mb-2 min-w-max flex flex-row items-end justify-center">
|
|
||||||
<Button
|
|
||||||
text={t('section-incident:add-contact') as string}
|
|
||||||
onButtonPressed={openAddIncidentContactModal}
|
|
||||||
color="mineshaft"
|
|
||||||
size="md"
|
|
||||||
icon={faPlus}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="h-12 w-full max-w-5xl bg-white/5 mt-2 flex items-center rounded-t-md flwex-row">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="bg-white/5 rounded-tl-md py-4 pl-4 pr-2 text-gray-400"
|
|
||||||
icon={faMagnifyingGlass}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="pl-2 text-gray-400 rounded-tr-md bg-white/5 w-full h-full outline-none"
|
|
||||||
value={searchIncidentContact}
|
|
||||||
onChange={(e) => setSearchIncidentContact(e.target.value)}
|
|
||||||
placeholder={t('common:search') as string}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{incidentContacts?.filter((incidentEmail) =>
|
|
||||||
incidentEmail.includes(searchIncidentContact)
|
|
||||||
).length > 0 ? (
|
|
||||||
incidentContacts
|
|
||||||
.filter((incidentEmail) => incidentEmail.includes(searchIncidentContact))
|
|
||||||
.map((contact) => (
|
|
||||||
<div
|
|
||||||
key={guidGenerator()}
|
|
||||||
className="flex flex-row items-center justify-between max-w-5xl px-4 py-3 hover:bg-white/5 border-t border-gray-600 w-full"
|
|
||||||
>
|
|
||||||
<p className="text-gray-300">{contact}</p>
|
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200">
|
|
||||||
<Button
|
|
||||||
onButtonPressed={() => deleteIncidentContactFully(contact)}
|
|
||||||
color="red"
|
|
||||||
size="icon-sm"
|
|
||||||
icon={faX}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="w-full flex flex-row justify-center mt-6 max-w-4xl ml-6">
|
|
||||||
<p className="text-gray-400">{t('section-incident:no-incident-contacts')}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <div className="border-l border-red pb-4 pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pt-2 max-w-6xl">
|
|
||||||
<p className="text-xl font-bold text-red">
|
|
||||||
Danger Zone
|
|
||||||
</p>
|
|
||||||
<p className="mt-4 text-md text-gray-400">
|
|
||||||
As soon as you delete an organization, you will
|
|
||||||
not be able to undo it. This will immediately
|
|
||||||
remove all organization members and cancel your
|
|
||||||
subscription. If you still want to do that,
|
|
||||||
please enter the name of the organization below.
|
|
||||||
</p>
|
|
||||||
<div className="max-h-28 w-full max-w-xl mr-auto mt-8 max-w-xl">
|
|
||||||
<InputField
|
|
||||||
label="Organization to be Deleted"
|
|
||||||
onChangeHandler={
|
|
||||||
setWorkspaceToBeDeletedName
|
|
||||||
}
|
|
||||||
type="varName"
|
|
||||||
value={workspaceToBeDeletedName}
|
|
||||||
placeholder=""
|
|
||||||
isRequired
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mt-6 w-full max-w-xl inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-bold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
|
||||||
onClick={executeDeletingWorkspace}
|
|
||||||
>
|
|
||||||
Delete Project
|
|
||||||
</button>
|
|
||||||
<p className="mt-0.5 ml-1 text-xs text-gray-500">
|
|
||||||
Note: You can only delete a project in case you
|
|
||||||
have more than one.
|
|
||||||
</p>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { plans } from 'public/data/frequentConstants';
|
||||||
|
|
||||||
|
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||||
|
import NavHeader from '@app/components/navigation/NavHeader';
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
encryptAssymmetric
|
||||||
|
} from '@app/components/utilities/cryptography/crypto';
|
||||||
|
import { useOrganization, useSubscription, useUser, useWorkspace } from '@app/context';
|
||||||
|
import {
|
||||||
|
useAddIncidentContact,
|
||||||
|
useAddUserToOrg,
|
||||||
|
useDeleteIncidentContact,
|
||||||
|
useDeleteOrgMembership,
|
||||||
|
useGetOrgIncidentContact,
|
||||||
|
useGetOrgUsers,
|
||||||
|
useGetUserWorkspaceMemberships,
|
||||||
|
useGetUserWsKey,
|
||||||
|
useRenameOrg,
|
||||||
|
useUpdateOrgUserRole,
|
||||||
|
useUploadWsKey
|
||||||
|
} from '@app/hooks/api';
|
||||||
|
|
||||||
|
import { OrgIncidentContactsTable, OrgMembersTable, OrgNameChangeSection } from './components';
|
||||||
|
|
||||||
|
export const OrgSettingsPage = () => {
|
||||||
|
const host = window.location.origin;
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { user } = useUser();
|
||||||
|
const { subscriptionPlan } = useSubscription();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
|
const orgId = currentOrg?._id || '';
|
||||||
|
const { data: orgUsers } = useGetOrgUsers(orgId);
|
||||||
|
const { data: workspaceMemberships } = useGetUserWorkspaceMemberships(orgId);
|
||||||
|
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
|
||||||
|
const { data: incidentContact } = useGetOrgIncidentContact(orgId);
|
||||||
|
|
||||||
|
const renameOrg = useRenameOrg();
|
||||||
|
const removeUserOrgMembership = useDeleteOrgMembership();
|
||||||
|
const addUserToOrg = useAddUserToOrg();
|
||||||
|
const updateOrgUserRole = useUpdateOrgUserRole();
|
||||||
|
const uploadWsKey = useUploadWsKey();
|
||||||
|
const addIncidentContact = useAddIncidentContact();
|
||||||
|
const removeIncidentContact = useDeleteIncidentContact();
|
||||||
|
|
||||||
|
const isMoreUsersNotAllowed =
|
||||||
|
(orgUsers || []).length >= 5 &&
|
||||||
|
subscriptionPlan === plans.starter &&
|
||||||
|
host === 'https://app.infisical.com';
|
||||||
|
|
||||||
|
const onRenameOrg = async (name: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await renameOrg.mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully renamed organization',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to rename organization',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveUserOrgMembership = async (membershipId: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId });
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully removed used from org',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to remove user from org',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onAddUserToOrg = async (email: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email });
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully invited user to org',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to invite user to org',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateOrgUserRole = 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 onGrantUserAccess = async (userId: 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,
|
||||||
|
nonce,
|
||||||
|
encryptedKey: ciphertext,
|
||||||
|
workspaceId: currentWorkspace?._id || ''
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to grant access to user',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddIncidentContact = async (email: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addIncidentContact.mutateAsync({ orgId, email });
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully added incident contact',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to add incident contact',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveIncidentContact = async (email: string) => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeIncidentContact.mutateAsync({ orgId, email });
|
||||||
|
createNotification({
|
||||||
|
text: 'Successfully removed incident contact',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to remove incident contact',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function deleted a workspace.
|
||||||
|
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
|
||||||
|
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
|
||||||
|
* It then deletes the workspace and forwards the user to another aviable workspace.
|
||||||
|
*/
|
||||||
|
// const executeDeletingWorkspace = async () => {
|
||||||
|
// const userWorkspaces = await getWorkspaces();
|
||||||
|
//
|
||||||
|
// if (userWorkspaces.length > 1) {
|
||||||
|
// if (
|
||||||
|
// userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
|
||||||
|
// workspaceToBeDeletedName
|
||||||
|
// ) {
|
||||||
|
// await deleteWorkspace(workspaceId);
|
||||||
|
// const ws = await getWorkspaces();
|
||||||
|
// router.push(`/dashboard/${ws[0]._id}`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
|
<NavHeader pageName={t('settings-org:title')} />
|
||||||
|
<div className="my-8 ml-6 flex max-w-5xl flex-row items-center justify-between text-xl">
|
||||||
|
<div className="flex flex-col items-start justify-start text-3xl">
|
||||||
|
<p className="mr-4 font-semibold text-gray-200">{t('settings-org:title')}</p>
|
||||||
|
<p className="mr-4 text-base font-normal text-gray-400">
|
||||||
|
{t('settings-org:description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50">
|
||||||
|
<OrgNameChangeSection orgName={currentOrg?.name} onOrgNameChange={onRenameOrg} />
|
||||||
|
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
|
||||||
|
<p className="mr-4 text-xl font-semibold text-white">
|
||||||
|
{t('section-members:org-members')}
|
||||||
|
</p>
|
||||||
|
<p className="mr-4 mt-2 mb-2 text-gray-400">
|
||||||
|
{t('section-members:org-members-description')}
|
||||||
|
</p>
|
||||||
|
<OrgMembersTable
|
||||||
|
isMoreUserNotAllowed={isMoreUsersNotAllowed}
|
||||||
|
orgName={currentOrg?.name || ''}
|
||||||
|
members={orgUsers}
|
||||||
|
workspaceMemberships={workspaceMemberships}
|
||||||
|
onInviteMember={onAddUserToOrg}
|
||||||
|
userId={user?._id || ''}
|
||||||
|
onRemoveMember={onRemoveUserOrgMembership}
|
||||||
|
onRoleChange={onUpdateOrgUserRole}
|
||||||
|
onGrantAccess={onGrantUserAccess}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
|
||||||
|
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
|
||||||
|
<div className="flex w-full max-w-3xl flex-col justify-between">
|
||||||
|
<p className="mb-3 min-w-max text-xl font-semibold">
|
||||||
|
{t('section-incident:incident-contacts')}
|
||||||
|
</p>
|
||||||
|
<p className="mb-2 min-w-max text-xs text-gray-500">
|
||||||
|
{t('section-incident:incident-contacts-description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<OrgIncidentContactsTable
|
||||||
|
contacts={incidentContact}
|
||||||
|
onRemoveContact={onRemoveIncidentContact}
|
||||||
|
onAddContact={onAddIncidentContact}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div className="border-l border-red pb-4 pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pt-2 max-w-6xl">
|
||||||
|
<p className="text-xl font-bold text-red">
|
||||||
|
Danger Zone
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-md text-gray-400">
|
||||||
|
As soon as you delete an organization, you will
|
||||||
|
not be able to undo it. This will immediately
|
||||||
|
remove all organization members and cancel your
|
||||||
|
subscription. If you still want to do that,
|
||||||
|
please enter the name of the organization below.
|
||||||
|
</p>
|
||||||
|
<div className="max-h-28 w-full max-w-xl mr-auto mt-8 max-w-xl">
|
||||||
|
<InputField
|
||||||
|
label="Organization to be Deleted"
|
||||||
|
onChangeHandler={
|
||||||
|
setWorkspaceToBeDeletedName
|
||||||
|
}
|
||||||
|
type="varName"
|
||||||
|
value={workspaceToBeDeletedName}
|
||||||
|
placeholder=""
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-6 w-full max-w-xl inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-bold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
|
onClick={executeDeletingWorkspace}
|
||||||
|
>
|
||||||
|
Delete Project
|
||||||
|
</button>
|
||||||
|
<p className="mt-0.5 ml-1 text-xs text-gray-500">
|
||||||
|
Note: You can only delete a project in case you
|
||||||
|
have more than one.
|
||||||
|
</p>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from '@app/components/v2';
|
||||||
|
import { usePopUp } from '@app/hooks';
|
||||||
|
import { IncidentContact } from '@app/hooks/api/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contacts?: IncidentContact[];
|
||||||
|
onRemoveContact: (email: string) => Promise<void>;
|
||||||
|
onAddContact: (email: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addContactFormSchema = yup.object({
|
||||||
|
email: yup.string().email().required().label('Email').trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
|
||||||
|
|
||||||
|
export const OrgIncidentContactsTable = ({
|
||||||
|
contacts = [],
|
||||||
|
onAddContact,
|
||||||
|
onRemoveContact
|
||||||
|
}: Props) => {
|
||||||
|
const [searchContact, setSearchContact] = useState('');
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
'addContact',
|
||||||
|
'removeContact'
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
|
||||||
|
|
||||||
|
const onAddIncidentContact = ({ email }: TAddContactForm) => {
|
||||||
|
onAddContact(email);
|
||||||
|
handlePopUpClose('addContact');
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveIncidentContact = async () => {
|
||||||
|
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
|
||||||
|
await onRemoveContact(incidentContactEmail);
|
||||||
|
handlePopUpClose('removeContact');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex">
|
||||||
|
<div className="mr-4 flex-1">
|
||||||
|
<Input
|
||||||
|
value={searchContact}
|
||||||
|
onChange={(e) => setSearchContact(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search incident contact by email..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen('addContact')}
|
||||||
|
>
|
||||||
|
Add Contact
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Email</Th>
|
||||||
|
<Th aria-label="actions" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{contacts
|
||||||
|
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
|
||||||
|
?.map(({ email }) => (
|
||||||
|
<Tr key={email}>
|
||||||
|
<Td className="w-full">{email}</Td>
|
||||||
|
<Td className="mr-4">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => handlePopUpOpen('removeContact', { email })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addContact?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle('addContact', isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Add an Incident Contact"
|
||||||
|
subTitle="This contact will be notified in the unlikely event of a severe incident."
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onAddIncidentContact)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="email"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Add Incident Contact
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpClose('addContact')}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.removeContact.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title="Do you want to remove this email from incident contact?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle('removeContact', isOpen)}
|
||||||
|
onDeleteApproved={onRemoveIncidentContact}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
|
||||||
+274
@@ -0,0 +1,274 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
Tag,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from '@app/components/v2';
|
||||||
|
import { usePopUp } from '@app/hooks';
|
||||||
|
import { OrgUser, Workspace } from '@app/hooks/api/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
members?: OrgUser[];
|
||||||
|
workspaceMemberships?: Record<string, Workspace[]>;
|
||||||
|
orgName: string;
|
||||||
|
isMoreUserNotAllowed: boolean;
|
||||||
|
onRemoveMember: (userId: string) => Promise<void>;
|
||||||
|
onInviteMember: (email: string) => Promise<void>;
|
||||||
|
onRoleChange: (membershipId: string, role: string) => Promise<void>;
|
||||||
|
onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
|
||||||
|
// the current user id to block remove org button
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMemberFormSchema = yup.object({
|
||||||
|
email: yup.string().email().required().label('Email').trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
|
||||||
|
|
||||||
|
export const OrgMembersTable = ({
|
||||||
|
members = [],
|
||||||
|
workspaceMemberships = {},
|
||||||
|
orgName,
|
||||||
|
isMoreUserNotAllowed,
|
||||||
|
onRemoveMember,
|
||||||
|
onInviteMember,
|
||||||
|
onGrantAccess,
|
||||||
|
onRoleChange,
|
||||||
|
userId
|
||||||
|
}: Props) => {
|
||||||
|
const [searchMemberFilter, setSearchMemberFilter] = useState('');
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
'addMember',
|
||||||
|
'removeMember',
|
||||||
|
'upgradePlan'
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
||||||
|
|
||||||
|
const onAddMember = ({ email }: TAddMemberForm) => {
|
||||||
|
onInviteMember(email);
|
||||||
|
handlePopUpClose('addMember');
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveOrgMemberApproved = async () => {
|
||||||
|
const orgMembershipId = (popUp?.removeMember?.data as { id: string })?.id;
|
||||||
|
await onRemoveMember(orgMembershipId);
|
||||||
|
handlePopUpClose('removeMember');
|
||||||
|
};
|
||||||
|
|
||||||
|
const isIamOwner = useMemo(
|
||||||
|
() => members.find(({ user }) => userId === user?._id)?.role === 'owner',
|
||||||
|
[userId, members]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterdUser = useMemo(
|
||||||
|
() =>
|
||||||
|
members.filter(
|
||||||
|
({ user, inviteEmail }) =>
|
||||||
|
user?.firstName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
user?.lastName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
user?.email?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
|
inviteEmail?.includes(searchMemberFilter)
|
||||||
|
),
|
||||||
|
[members, searchMemberFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
if (isMoreUserNotAllowed) {
|
||||||
|
handlePopUpOpen('upgradePlan');
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen('addMember');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
{filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
|
||||||
|
const name = user ? `${user.firstName} ${user.lastName}` : '-';
|
||||||
|
const email = user?.email || inviteEmail;
|
||||||
|
const userWs = workspaceMemberships?.[user?._id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={`org-membership-${orgMembershipId}`}>
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{email}</Td>
|
||||||
|
<Td>
|
||||||
|
{status === 'accepted' && (
|
||||||
|
<Select
|
||||||
|
defaultValue={role}
|
||||||
|
isDisabled={userId === user?._id}
|
||||||
|
className="w-full bg-mineshaft-600"
|
||||||
|
onValueChange={(selectedRole) =>
|
||||||
|
onRoleChange(orgMembershipId, selectedRole)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(isIamOwner || role === 'owner') && (
|
||||||
|
<SelectItem value="owner">owner</SelectItem>
|
||||||
|
)}
|
||||||
|
<SelectItem value="admin">admin</SelectItem>
|
||||||
|
<SelectItem value="member">member</SelectItem>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
{(status === 'invited' || status === 'verified') && (
|
||||||
|
<Button colorSchema="secondary" onClick={() => onInviteMember(email)}>
|
||||||
|
Resent Invite
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{status === 'completed' && (
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => onGrantAccess(user?._id, user?.publicKey)}
|
||||||
|
>
|
||||||
|
Grant Access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{userWs ? (
|
||||||
|
userWs?.map(({ name: wsName, _id }) => (
|
||||||
|
<Tag key={`user-${user._id}-workspace-${_id}`} className="my-1">
|
||||||
|
{wsName}
|
||||||
|
</Tag>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Tag colorSchema="red">This user isn't part of any projects yet</Tag>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
colorSchema="danger"
|
||||||
|
isDisabled={userId === user?._id}
|
||||||
|
onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addMember?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle('addMember', isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={`Invite others to ${orgName}`}
|
||||||
|
subTitle={
|
||||||
|
<>
|
||||||
|
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.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</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."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgMembersTable } from './OrgMembersTable';
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
import { Button, FormControl, Input } from '@app/components/v2';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
orgName?: string;
|
||||||
|
onOrgNameChange: (name: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = yup.object({
|
||||||
|
name: yup.string().required().label('Project Name')
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = yup.InferType<typeof formSchema>;
|
||||||
|
|
||||||
|
export const OrgNameChangeSection = ({ onOrgNameChange, orgName }: Props): JSX.Element => {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
formState: { isDirty, isSubmitting }
|
||||||
|
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset({ name: orgName });
|
||||||
|
}, [orgName]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name }: FormData) => {
|
||||||
|
await onOrgNameChange(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-3">
|
||||||
|
<p className="mb-4 mt-2 text-xl font-semibold">{t('common:display-name')}</p>
|
||||||
|
<div className="mb-2 w-full max-w-lg">
|
||||||
|
<Controller
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input placeholder="Type your org name" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
color="mineshaft"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isDisabled={!isDirty || isSubmitting}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faCheck} />}
|
||||||
|
>
|
||||||
|
{t('common:save-changes')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgNameChangeSection } from './OrgNameChangeSection';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
|
||||||
|
export { OrgMembersTable } from './OrgMembersTable';
|
||||||
|
export { OrgNameChangeSection } from './OrgNameChangeSection';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { OrgSettingsPage } from './OrgSettingsPage';
|
||||||
Reference in New Issue
Block a user