mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 19:28:09 +00:00
feat(ui): completed org settings page revamp
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>
|
||||||
|
|||||||
@@ -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