Updated the dashabord, members, and settings pages

This commit is contained in:
Vladyslav Matsiiako
2023-02-12 17:54:22 -08:00
parent 2022988e77
commit 17f9e53779
20 changed files with 252 additions and 98 deletions
@@ -397,9 +397,21 @@ export const getOrganizationMembersAndTheirWorkspaces = async (
res: Response res: Response
) => { ) => {
const { organizationId } = req.params; const { organizationId } = req.params;
const orgMemberships = await MembershipOrg.find({ organization: organizationId });
const userIds = orgMemberships.map(orgMembership => orgMembership.user); const workspacesSet = (
const memberships = await Membership.find({ user: { $in: userIds } }); await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString());
const memberships = (
await Membership.find({
workspace: { $in: workspacesSet }
}).populate('workspace')
);
const userToWorkspaceIds: any = {}; const userToWorkspaceIds: any = {};
memberships.forEach(membership => { memberships.forEach(membership => {
@@ -411,15 +423,5 @@ export const getOrganizationMembersAndTheirWorkspaces = async (
} }
}); });
const workspaceIds = Object.values(userToWorkspaceIds).flat() return res.json(userToWorkspaceIds);
const workspacesList = await Workspace.find({
organization: organizationId,
_id: { $in: workspaceIds }
});
const populatedUserWorkspaces = _.mapValues(userToWorkspaceIds, workspaceIds =>
_.map(workspaceIds, id => _.find(workspacesList, { _id: id }))
);
return res.json(populatedUserWorkspaces);
}; };
@@ -246,13 +246,14 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (
relatedWorkspace.environments.forEach(environment => { relatedWorkspace.environments.forEach(environment => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ }) const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE }) const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE })
if (isReadBlocked) { if (isReadBlocked && isWriteBlocked) {
return return
} else { } else {
accessibleEnvironments.push({ accessibleEnvironments.push({
name: environment.name, name: environment.name,
slug: environment.slug, slug: environment.slug,
isWriteDenied: isWriteBlocked isWriteDenied: isWriteBlocked,
isReadDenied: isReadBlocked
}) })
} }
}) })
+1 -1
View File
@@ -10,7 +10,7 @@ export interface Tag {
export interface SecretDataProps { export interface SecretDataProps {
pos: number; pos: number;
key: string; key: string;
value: string; value: string | undefined;
valueOverride: string | undefined; valueOverride: string | undefined;
id: string; id: string;
comment: string; comment: string;
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
import { plans } from 'public/data/frequentConstants'; import { plans } from 'public/data/frequentConstants';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
@@ -106,6 +106,11 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
ability: "read", ability: "read",
environmentSlug: slug environmentSlug: slug
}]; }];
} else if (val === "Add Only") {
denials = [{
ability: "read",
environmentSlug: slug
}];
} else { } else {
denials = []; denials = [];
} }
@@ -185,21 +190,21 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
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 bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max">
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" /> <div className="absolute rounded-t-md w-full h-[3.1rem] 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="w-full my-0.5">
<thead className="text-gray-400 text-sm font-light"> <thead className="text-gray-400 text-xs font-light">
<tr> <tr>
<th className="text-left pl-4 py-3.5">NAME</th> <th className="text-left pl-4 py-3.5">NAME</th>
<th className="text-left pl-4 py-3.5">EMAIL</th> <th className="text-left pl-4 py-3.5">EMAIL</th>
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th> <th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
{workspaceEnvs.map(env => ( {workspaceEnvs.map(env => (
<th key={guidGenerator()} className="text-left pl-8 py-1 max-w-min break-normal"> <th key={guidGenerator()} className="text-left pl-2 py-1 max-w-min break-normal">
<span>{env.name.toUpperCase()}<br/></span> <span>{env.slug.toUpperCase()}<br/></span>
{/* <span>PERMISSION</span> */} {/* <span>PERMISSION</span> */}
</th> </th>
))} ))}
@@ -221,7 +226,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
user.email?.toLowerCase().includes(filter) user.email?.toLowerCase().includes(filter)
) )
.map((row, index) => ( .map((row, index) => (
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-700"> <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="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
{row.firstName} {row.lastName} {row.firstName} {row.lastName}
</td> </td>
@@ -231,7 +236,8 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300"> <td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
<div className="justify-start h-full flex flex-row items-center"> <div className="justify-start h-full flex flex-row items-center">
<Select <Select
className="w-36" className="w-36 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}
@@ -253,23 +259,36 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
)} )}
</div> </div>
</td> </td>
{workspaceEnvs.map((env) => <td key={guidGenerator()} className="pl-8 py-2 border-mineshaft-700 border-t text-gray-300"> {workspaceEnvs.map((env) => <td key={guidGenerator()} className="pl-2 py-2 border-mineshaft-700 border-t text-gray-300">
<Select <Select
className="w-36" className="w-16 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-700"
position="item-aligned"
// open={isOpen} // open={isOpen}
onValueChange={(val) => handlePermissionUpdate(index, val, row.membershipId, env.slug)} onValueChange={(val) => handlePermissionUpdate(index, val, row.membershipId, env.slug)}
value={ value={
// eslint-disable-next-line no-nested-ternary // 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")) (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" ? "No Access"
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") ? "Read Only" : "Read & Write") // 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'} disabled={myRole !== 'admin'}
// onOpenChange={(open) => setIsOpen(open)} // onOpenChange={(open) => setIsOpen(open)}
> >
<SelectItem value="No Access">No Access</SelectItem> <SelectItem value="No Access" customIcon={faEyeSlash}>No Access</SelectItem>
<SelectItem value="Read Only">Read Only</SelectItem> <SelectItem value="Read Only" customIcon={faEye}>Read Only</SelectItem>
<SelectItem value="Read & Write">Read & Write</SelectItem> <SelectItem value="Add Only" customIcon={faPlus}>Add Only</SelectItem>
<SelectItem value="Read & Write" customIcon={faPenToSquare}>Read & Write</SelectItem>
</Select> </Select>
</td>)} </td>)}
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700"> <td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
@@ -4,6 +4,7 @@ import { faX } from '@fortawesome/free-solid-svg-icons';
import changeUserRoleInOrganization from '@app/pages/api/organization/changeUserRoleInOrganization'; import changeUserRoleInOrganization from '@app/pages/api/organization/changeUserRoleInOrganization';
import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization'; import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization';
import getOrganizationProjectMemberships from '@app/pages/api/organization/GetOrgProjectMemberships';
import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace'; import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey'; import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import uploadKeys from '@app/pages/api/workspace/uploadKeys'; import uploadKeys from '@app/pages/api/workspace/uploadKeys';
@@ -36,6 +37,8 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
); );
const router = useRouter(); const router = useRouter();
const [myRole, setMyRole] = useState('member'); const [myRole, setMyRole] = useState('member');
const [userProjectMemberships, setUserProjectMemberships] = useState<any[]>([]);
console.log(123, userData)
const workspaceId = router.query.id as string; const workspaceId = router.query.id as string;
// Delete the row in the table (e.g. a user) // Delete the row in the table (e.g. a user)
@@ -79,6 +82,10 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
useEffect(() => { useEffect(() => {
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role); setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
(async () => {
const result = await getOrganizationProjectMemberships({ orgId: String(localStorage.getItem("orgData.id"))})
setUserProjectMemberships(result);
})();
}, [userData, myUser]); }, [userData, myUser]);
const grantAccess = async (id: string, publicKey: string) => { const grantAccess = async (id: string, publicKey: string) => {
@@ -110,7 +117,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
}; };
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 bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max w-full">
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" /> <div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" />
<table className="w-full my-0.5"> <table className="w-full my-0.5">
<thead className="text-gray-400 text-sm font-light"> <thead className="text-gray-400 text-sm font-light">
@@ -118,6 +125,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
<th className="text-left pl-4 py-3.5">NAME</th> <th className="text-left pl-4 py-3.5">NAME</th>
<th className="text-left pl-4 py-3.5">EMAIL</th> <th className="text-left pl-4 py-3.5">EMAIL</th>
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th> <th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
<th className="text-left pl-6 pr-10 py-3.5">PROJECTS</th>
<th aria-label="buttons" /> <th aria-label="buttons" />
</tr> </tr>
</thead> </thead>
@@ -189,6 +197,17 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
)} )}
</div> </div>
</td> </td>
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="flex items-center max-h-16 overflow-x-auto w-full max-w-xl">
{userProjectMemberships[row.userId]
? userProjectMemberships[row.userId]?.map((project: any) => (
<div key={project.id} className='mx-1 min-w-max px-1.5 bg-mineshaft-500 rounded-sm text-sm text-bunker-200 flex items-center'>
<span className='mb-0.5 cursor-default'>{project.name}</span>
</div>
))
: <span className='ml-1 text-bunker-100 rounded-sm px-1 py-0.5 text-sm bg-red/80'>This user isn&apos;t part of any projects yet.</span>}
</td>
</td>
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700"> <td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
{myUser !== row.email && {myUser !== row.email &&
// row.role !== "admin" && // row.role !== "admin" &&
@@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
type NotificationType = 'success' | 'error' | 'info'; type NotificationType = 'success' | 'error' | 'info';
@@ -36,7 +36,7 @@ const Notification = ({ notification, clearNotification }: NotificationProps) =>
return ( return (
<div <div
className="relative w-full flex items-center justify-between px-4 py-4 rounded-md border border-bunker-500 pointer-events-auto bg-bunker-500" className="relative w-full flex items-center justify-between px-6 py-6 rounded-md border border-bunker-500 pointer-events-auto bg-mineshaft-700 mb-3 right-3"
role="alert" role="alert"
> >
{notification.type === 'error' && ( {notification.type === 'error' && (
@@ -48,13 +48,13 @@ const Notification = ({ notification, clearNotification }: NotificationProps) =>
{notification.type === 'info' && ( {notification.type === 'info' && (
<div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md" /> <div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md" />
)} )}
<p className="text-bunker-200 text-sm font-semibold mt-0.5">{notification.text}</p> <p className="text-bunker-200 text-md font-base mt-0.5">{notification.text}</p>
<button <button
type="button" type="button"
className="rounded-lg" className="rounded-lg"
onClick={() => clearNotification(notification.text)} onClick={() => clearNotification(notification.text)}
> >
<FontAwesomeIcon className="text-white pl-2 w-4 h-3 hover:text-red" icon={faX} /> <FontAwesomeIcon className="absolute right-2 top-3 text-bunker-300 pl-2 w-4 h-4 hover:text-white" icon={faXmark} />
</button> </button>
</div> </div>
); );
@@ -1,9 +1,10 @@
import { memo, SyntheticEvent, useRef } from 'react'; import { memo, SyntheticEvent, useRef } from 'react';
import { faCircle, faExclamationCircle, faEye, faLayerGroup } from '@fortawesome/free-solid-svg-icons'; import { faCircle, faCodeBranch, faExclamationCircle, faEye } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../utilities/randomId'; import guidGenerator from '../utilities/randomId';
import { HoverObject } from '../v2/HoverCard'; import { HoverObject } from '../v2/HoverCard';
import { PopoverObject } from '../v2/Popover/Popover';
const REGEX = /([$]{.*?})/g; const REGEX = /([$]{.*?})/g;
@@ -112,7 +113,7 @@ const DashboardInputField = ({
}}> }}>
<HoverObject <HoverObject
text={overrideEnabled ? 'This secret is overriden with your personal value' : 'You can override this secret with a personal value'} text={overrideEnabled ? 'This secret is overriden with your personal value' : 'You can override this secret with a personal value'}
icon={faLayerGroup} icon={faCodeBranch}
color={overrideEnabled ? 'primary' : 'bunker-400'} color={overrideEnabled ? 'primary' : 'bunker-400'}
/> />
</button> </button>
@@ -125,24 +126,24 @@ const DashboardInputField = ({
const error = startsWithNumber || isDuplicate; const error = startsWithNumber || isDuplicate;
return ( return (
<div title={value} className={`relative flex-col w-full h-10 ${ <PopoverObject text={value || ''} onChangeHandler={onChangeHandler} position={position}>
isSideBarOpen && 'bg-mineshaft-700 duration-200' <div title={value} className={`relative flex-col w-full h-10 overflow-hidden ${
}`}> isSideBarOpen && 'bg-mineshaft-700 duration-200'
<div }`}>
className={`group relative flex flex-col justify-center items-center ${ <div
error ? 'w-max' : 'w-full' className={`group relative flex flex-col justify-center items-center h-full ${
}`} error ? 'w-max' : 'w-full'
> }`}
<input >
onChange={(e) => onChangeHandler(e.target.value, position)} {value?.split("\n")[0] ? <span className='ph-no-capture truncate break-all bg-transparent leading-tight text-xs px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'>
type={type} {value?.split("\n")[0]}
value={value} </span> : <span className='text-bunker-400'>-</span> }
className='z-10 peer ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200' {value?.split("\n")[1] && <span className='ph-no-capture truncate break-all bg-transparent leading-tight text-xs px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'>
spellCheck="false" {value?.split("\n")[1]}
placeholder='–' </span>}
/> </div>
</div> </div>
</div> </PopoverObject>
); );
} }
if (type === 'value') { if (type === 'value') {
@@ -215,7 +216,7 @@ const DashboardInputField = ({
))} ))}
{value?.split('').length === 0 && <span className='text-bunker-400/80'>EMPTY</span>} {value?.split('').length === 0 && <span className='text-bunker-400/80'>EMPTY</span>}
</div> </div>
<div className='invisible group-hover:visible cursor-pointer'><FontAwesomeIcon icon={faEye} /></div> <div className='invisible group-hover:visible cursor-default z-[100]'><FontAwesomeIcon icon={faEye} /></div>
</div> </div>
)} )}
</div> </div>
+16 -2
View File
@@ -132,7 +132,7 @@ const KeyPair = ({
/> />
</div> </div>
</div> </div>
<div className="w-2/12 border-r border-mineshaft-600"> <div className="w-[calc(10%)] border-r border-mineshaft-600">
<div className="flex items-center max-h-16"> <div className="flex items-center max-h-16">
<DashboardInputField <DashboardInputField
onChangeHandler={modifyComment} onChangeHandler={modifyComment}
@@ -171,12 +171,26 @@ const KeyPair = ({
<FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} /> <FontAwesomeIcon className="text-bunker-300 hover:text-primary text-lg" icon={faEllipsis} />
</div> </div>
<div className={`group-hover:bg-mineshaft-700 z-50 ${isSnapshot ?? 'invisible'}`}> <div className={`group-hover:bg-mineshaft-700 z-50 ${isSnapshot ?? 'invisible'}`}>
<DeleteActionButton {keyPair.key || keyPair.value
? <DeleteActionButton
onSubmit={() => { if (deleteRow) { onSubmit={() => { if (deleteRow) {
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key }) deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
}}} }}}
isPlain isPlain
/> />
: <div className='cursor-pointer w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => { if (deleteRow) {
deleteRow({ ids: [keyPair.id], secretName: keyPair?.key })
}}}
className="invisible group-hover:visible"
>
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5" icon={faXmark} />
</div>
</div>}
</div> </div>
</div> </div>
</div> </div>
@@ -18,7 +18,7 @@ import GenerateSecretMenu from './GenerateSecretMenu';
interface SecretProps { interface SecretProps {
key: string; key: string;
value: string; value: string | undefined;
valueOverride: string | undefined; valueOverride: string | undefined;
pos: number; pos: number;
id: string; id: string;
@@ -80,9 +80,9 @@ const SideBar = ({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-[28rem] sticky top-0 right-0 z-[70] shadow-xl flex flex-col justify-between"> <div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-full min-w-sm max-w-sm sticky top-0 right-0 z-[70] shadow-xl flex flex-col justify-between">
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full w-full">
<Image <Image
src="/images/loading/loading.gif" src="/images/loading/loading.gif"
height={60} height={60}
@@ -91,7 +91,7 @@ const SideBar = ({
/> />
</div> </div>
) : ( ) : (
<div className="h-min overflow-y-auto"> <div className="h-min overflow-y-auto w-full">
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center"> <div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
<p className="font-semibold text-lg text-bunker-200">{t('dashboard:sidebar.secret')}</p> <p className="font-semibold text-lg text-bunker-200">{t('dashboard:sidebar.secret')}</p>
<div <div
@@ -186,7 +186,7 @@ const SideBar = ({
/> />
</div> </div>
)} )}
<div className="mt-full mt-4 mb-4 flex max-w-sm flex-col justify-start space-y-2 px-4"> <div className="mt-full w-96 mt-4 mb-4 flex max-w-sm flex-col justify-start space-y-2 px-4">
<div> <div>
<Button <Button
text="Compare secret across environments" text="Compare secret across environments"
@@ -197,7 +197,7 @@ const SideBar = ({
<CompareSecretsModal <CompareSecretsModal
compareModal={compareModal} compareModal={compareModal}
setCompareModal={setCompareModal} setCompareModal={setCompareModal}
currentSecret={{ key: data[0]?.key, value: data[0]?.value }} currentSecret={{ key: data[0]?.key, value: data[0]?.value ?? '' }}
workspaceEnvs={workspaceEnvs} workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv} selectedEnv={selectedEnv}
workspaceId={workspaceId} workspaceId={workspaceId}
@@ -76,7 +76,7 @@ const encryptSecrets = async ({
iv: secretValueIV, iv: secretValueIV,
tag: secretValueTag tag: secretValueTag
} = encryptSymmetric({ } = encryptSymmetric({
plaintext: secret.value, plaintext: secret.value ?? '',
key: randomBytes key: randomBytes
}); });
@@ -24,7 +24,7 @@ interface EncryptedSecretProps {
interface SecretProps { interface SecretProps {
key: string; key: string;
value: string; value: string | undefined;
type: 'personal' | 'shared'; type: 'personal' | 'shared';
comment: string; comment: string;
id: string; id: string;
@@ -87,12 +87,17 @@ const getSecretsForProject = async ({
key key
}); });
const plainTextValue = decryptSymmetric({ let plainTextValue;
ciphertext: secret.secretValueCiphertext, if (secret.secretValueCiphertext !== undefined) {
iv: secret.secretValueIV, plainTextValue = decryptSymmetric({
tag: secret.secretValueTag, ciphertext: secret.secretValueCiphertext,
key iv: secret.secretValueIV,
}); tag: secret.secretValueTag,
key
});
} else {
plainTextValue = undefined;
}
let plainTextComment; let plainTextComment;
if (secret.secretCommentCiphertext) { if (secret.secretCommentCiphertext) {
@@ -0,0 +1,49 @@
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as Popover from '@radix-ui/react-popover';
type Props = {
children: any;
text: string;
onChangeHandler: (value: string, position: number) => void;
position: number;
};
export type PopoverProps = Props;
export const PopoverObject = ({children, text, onChangeHandler, position}: Props) => (
<Popover.Root>
<Popover.Trigger asChild className='data-[state=open]:outline data-[state=open]:outline-primary data-[state=closed]:hover:outline data-[state=closed]:hover:outline-mineshaft-400'>
{children}
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
className="rounded z-[100] p-3 w-[460px] min-h-fit border border-chicago-700 bg-mineshaft-600 shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2)] focus:shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2),0_0_0_2px_theme(colors.violet7)] will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
sideOffset={5}
hideWhenDetached
side="left"
>
<div className="flex flex-col pt-2 dark">
<p className="text-bunker-200 text-[15px] leading-[0px] font-medium mb-5">Comment</p>
<textarea
onChange={(e) => onChangeHandler(e.target.value, position)}
// type={type}
value={text}
className='z-10 dark:[color-scheme:dark] peer h-[20rem] ph-no-capture bg-bunker-600 border border-mineshaft-500 rounded-md py-2.5 caret-bunker-200 text-sm px-2 w-full outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'
spellCheck="false"
placeholder='–'
/>
</div>
<Popover.Close
className="rounded-full h-[25px] w-[25px] inline-flex items-center justify-center text-bunker-300 hover:text-white absolute top-[5px] right-[5px] hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 outline-none cursor-default"
aria-label="Close"
>
<FontAwesomeIcon icon={faXmark} />
</Popover.Close>
<Popover.Arrow className="fill-chicago-700" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
PopoverObject.displayName = 'Popover';
@@ -0,0 +1,2 @@
export type { PopoverProps } from './Popover';
export { PopoverObject } from './Popover';
+9 -4
View File
@@ -1,4 +1,5 @@
import { forwardRef, ReactNode } from 'react'; import { forwardRef, ReactNode } from 'react';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { faCheck, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as SelectPrimitive from '@radix-ui/react-select'; import * as SelectPrimitive from '@radix-ui/react-select';
@@ -13,6 +14,7 @@ type Props = {
dropdownContainerClassName?: string; dropdownContainerClassName?: string;
isLoading?: boolean; isLoading?: boolean;
position?: 'item-aligned' | 'popper'; position?: 'item-aligned' | 'popper';
icon?: IconProp;
}; };
export type SelectProps = SelectPrimitive.SelectProps & Props; export type SelectProps = SelectPrimitive.SelectProps & Props;
@@ -32,7 +34,9 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
className className
)} )}
> >
<SelectPrimitive.Value placeholder={placeholder} /> <SelectPrimitive.Value placeholder={placeholder}>
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
</SelectPrimitive.Value>
{!props.disabled && ( {!props.disabled && (
<SelectPrimitive.Icon className="ml-3"> <SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon icon={faChevronDown} size="sm" /> <FontAwesomeIcon icon={faChevronDown} size="sm" />
@@ -42,7 +46,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
className={twMerge( className={twMerge(
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]', 'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 border border-mineshaft-500 drop-shadow-xl font-inter text-bunker-100 shadow-md z-[100]',
dropdownContainerClassName dropdownContainerClassName
)} )}
position={position} position={position}
@@ -76,6 +80,7 @@ Select.displayName = 'Select';
export type SelectItemProps = Omit<SelectPrimitive.SelectItemProps, 'disabled'> & { export type SelectItemProps = Omit<SelectPrimitive.SelectItemProps, 'disabled'> & {
isDisabled?: boolean; isDisabled?: boolean;
isSelected?: boolean; isSelected?: boolean;
customIcon?: IconProp;
}; };
export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>( export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
@@ -88,13 +93,13 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 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-gray-600', isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
className className
)} )}
ref={forwardedRef} ref={forwardedRef}
> >
<SelectPrimitive.ItemIndicator className="absolute left-3.5 text-primary"> <SelectPrimitive.ItemIndicator className="absolute left-3.5 text-primary">
<FontAwesomeIcon icon={faCheck} /> <FontAwesomeIcon icon={props.customIcon ? props.customIcon : faCheck} />
</SelectPrimitive.ItemIndicator> </SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
@@ -159,9 +159,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
return ( return (
<div <div
className={`absolute border-l border-mineshaft-500 ${ className={`absolute border-l border-mineshaft-500 w-full min-w-sm max-w-sm ${
isLoading ? 'bg-bunker-800' : 'bg-bunker' isLoading ? 'bg-bunker-800' : 'bg-bunker'
} fixed h-full w-[28rem] right-0 z-[70] shadow-xl flex flex-col justify-between sticky top-0`} } fixed h-full right-0 z-[70] shadow-xl flex flex-col justify-between sticky top-0`}
> >
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full mb-8"> <div className="flex items-center justify-center h-full mb-8">
@@ -186,7 +186,8 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" /> <FontAwesomeIcon icon={faXmark} className="w-4 h-4 text-bunker-300 cursor-pointer" />
</div> </div>
</div> </div>
<div className="flex flex-col px-2 py-2 overflow-y-auto h-[92vh]"> <div className="flex flex-col w-96 px-2 py-2 overflow-y-auto bg-bunker border-l border-mineshaft-600 h-[calc(100vh-115px)]">
<span className='px-2 text-bunker-200 pb-2 text-sm'>Note: This will recover secrets for all enviroments in this project.</span>
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => ( {secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => (
<div <div
onKeyDown={() => null} onKeyDown={() => null}
@@ -76,9 +76,9 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
}, [secretId]); }, [secretId]);
return ( return (
<div className="w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none"> <div className="w-full min-w-40 h-[12.4rem] px-4 mt-4 text-sm text-bunker-300 overflow-x-none">
<p className="">{t('dashboard:sidebar.version-history')}</p> <p className="">{t('dashboard:sidebar.version-history')}</p>
<div className="p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full"> <div className="pl-1 py-0.5 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full">
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Image <Image
@@ -99,10 +99,10 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
<div className="p-1"> <div className="p-1">
<FontAwesomeIcon icon={index === 0 ? faDotCircle : faCircle} /> <FontAwesomeIcon icon={index === 0 ? faDotCircle : faCircle} />
</div> </div>
<div className="w-0 h-full border-l mt-1" /> <div className="w-0 h-full border-l border-bunker-300 mt-1" />
</div> </div>
<div className="flex flex-col w-full max-w-[calc(100%-2.3rem)]"> <div className="flex flex-col w-full max-w-[calc(100%-2.3rem)]">
<div className="pr-2 pt-1"> <div className="pr-2 pt-1 text-bunker-300/90">
{new Date(version.createdAt).toLocaleDateString('en-US', { {new Date(version.createdAt).toLocaleDateString('en-US', {
year: 'numeric', year: 'numeric',
month: '2-digit', month: '2-digit',
@@ -114,10 +114,10 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
</div> </div>
<div className=""> <div className="">
<p className="break-words ph-no-capture"> <p className="break-words ph-no-capture">
<span className="py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5"> <span className="py-0.5 px-1 rounded-sm bg-primary-500/30 mr-1.5">
Value: Value:
</span> </span>
{version.value} <span className='font-mono'>{version.value}</span>
</p> </p>
</div> </div>
</div> </div>
@@ -0,0 +1,24 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
/**
* This route lets us get all the project memebrships of users in an org.
* @param {*} req
* @param {*} res
* @returns
*/
const getOrganizationProjectMemberships = (req: { orgId: string }) =>
SecurityClient.fetchCall(`/api/v1/organization/${req.orgId}/workspace-memberships`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log('Failed to get project memberships for users in an org');
return undefined;
});
export default getOrganizationProjectMemberships;
+24 -12
View File
@@ -53,12 +53,13 @@ type WorkspaceEnv = {
name: string; name: string;
slug: string; slug: string;
isWriteDenied: boolean; isWriteDenied: boolean;
isReadDenied: boolean;
}; };
interface SecretDataProps { interface SecretDataProps {
pos: number; pos: number;
key: string; key: string;
value: string; value: string | undefined;
valueOverride: string | undefined; valueOverride: string | undefined;
id: string; id: string;
idOverride: string | undefined; idOverride: string | undefined;
@@ -268,6 +269,8 @@ export default function Dashboard() {
}); });
setInitialData(dataToSort); setInitialData(dataToSort);
reorderRows(dataToSort); reorderRows(dataToSort);
} else {
setIsLoading(false);
} }
} catch (error) { } catch (error) {
console.log('Error', error); console.log('Error', error);
@@ -318,7 +321,7 @@ export default function Dashboard() {
setButtonReady(true); setButtonReady(true);
toggleSidebar('None'); toggleSidebar('None');
createNotification({ createNotification({
text: `${secretName} has been deleted. Remember to save changes.`, text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`,
type: 'error' type: 'error'
}); });
sortValuesHandler( sortValuesHandler(
@@ -526,7 +529,7 @@ export default function Dashboard() {
}); });
if (secrets) await addSecrets({ secrets, env: selectedEnv.slug, workspaceId }); if (secrets) await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
} }
if (selectedEnv && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) { if (selectedEnv && !selectedEnv.isReadDenied && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
const secrets = await encryptSecrets({ const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated), secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
workspaceId, workspaceId,
@@ -642,6 +645,11 @@ export default function Dashboard() {
{new Date(snapshotData.createdAt).toLocaleString()} {new Date(snapshotData.createdAt).toLocaleString()}
</span> </span>
)} )}
{selectedEnv?.isReadDenied && (
<span className="bg-primary-500 text-black text-sm ml-4 mt-1 px-1.5 rounded-md">
Add Only Mode
</span>
)}
</div> </div>
{!snapshotData && data?.length === 0 && selectedEnv && ( {!snapshotData && data?.length === 0 && selectedEnv && (
<ListBox <ListBox
@@ -652,7 +660,8 @@ export default function Dashboard() {
workspaceEnvs.find(({ name }) => envName === name) || { workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown', name: 'unknown',
slug: 'unknown', slug: 'unknown',
isWriteDenied: false isWriteDenied: false,
isReadDenied: false
} }
) )
} }
@@ -661,7 +670,7 @@ export default function Dashboard() {
</div> </div>
<div className="flex flex-row"> <div className="flex flex-row">
<div className="flex justify-start max-w-sm mt-1 mr-2"> <div className="flex justify-start max-w-sm mt-1 mr-2">
<Button {!selectedEnv?.isReadDenied && <Button
text={String(`${numSnapshots} ${t('Commits')}`)} text={String(`${numSnapshots} ${t('Commits')}`)}
onButtonPressed={() => { onButtonPressed={() => {
toggleSidebar('None'); toggleSidebar('None');
@@ -670,7 +679,7 @@ export default function Dashboard() {
color="mineshaft" color="mineshaft"
size="md" size="md"
icon={faClockRotateLeft} icon={faClockRotateLeft}
/> />}
</div> </div>
{(data?.length !== 0 || buttonReady) && !snapshotData && ( {(data?.length !== 0 || buttonReady) && !snapshotData && (
<div className="flex justify-start max-w-sm mt-1"> <div className="flex justify-start max-w-sm mt-1">
@@ -738,7 +747,8 @@ export default function Dashboard() {
workspaceEnvs.find(({ name }) => envName === name) || { workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown', name: 'unknown',
slug: 'unknown', slug: 'unknown',
isWriteDenied: false isWriteDenied: false,
isReadDenied: false
} }
) )
} }
@@ -752,7 +762,8 @@ export default function Dashboard() {
workspaceEnvs.find(({ name }) => envName === name) || { workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown', name: 'unknown',
slug: 'unknown', slug: 'unknown',
isWriteDenied: false isWriteDenied: false,
isReadDenied: false
} }
) )
} }
@@ -770,7 +781,7 @@ export default function Dashboard() {
placeholder={String(t('dashboard:search-keys'))} placeholder={String(t('dashboard:search-keys'))}
/> />
</div> </div>
{!snapshotData && ( {!snapshotData && !selectedEnv.isReadDenied && (
<div className="ml-2 min-w-max flex flex-row items-start justify-start"> <div className="ml-2 min-w-max flex flex-row items-start justify-start">
<DownloadSecretMenu data={data} env={selectedEnv.slug} /> <DownloadSecretMenu data={data} env={selectedEnv.slug} />
</div> </div>
@@ -825,7 +836,7 @@ export default function Dashboard() {
> >
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center"> <div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
<div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center"> <div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center">
<div className='text-transparent text-xs flex items-center justify-center w-14 h-10 cursor-default'>0</div> <div className='text-transparent text-xs flex items-center justify-center w-12 h-10 cursor-default'>0</div>
<span className='px-2 text-bunker-300 font-semibold'>Key</span> <span className='px-2 text-bunker-300 font-semibold'>Key</span>
{!snapshotData && <IconButton {!snapshotData && <IconButton
ariaLabel="copy icon" ariaLabel="copy icon"
@@ -843,7 +854,7 @@ export default function Dashboard() {
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div> <div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div>
</div> </div>
</div> </div>
<div className="w-2/12 border-r border-mineshaft-600"> <div className="w-[calc(10%)] border-r border-mineshaft-600">
<div className="flex items-center max-h-16"> <div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div> <div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
</div> </div>
@@ -876,6 +887,7 @@ export default function Dashboard() {
|| row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase()) || row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase())
|| row.comment?.toUpperCase().includes(searchKeys.toUpperCase())) || row.comment?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter((row) => !sharedToHide.includes(row.id)) .filter((row) => !sharedToHide.includes(row.id))
.filter((row) => row.value !== undefined)
.map((keyPair) => ( .map((keyPair) => (
<KeyPair <KeyPair
isCapitalized={autoCapitalization} isCapitalized={autoCapitalization}
@@ -995,7 +1007,7 @@ export default function Dashboard() {
toggleSidebar={toggleSidebar} toggleSidebar={toggleSidebar}
data={data.filter( data={data.filter(
(row: SecretDataProps) => (row: SecretDataProps) =>
row.id === sidebarSecretId row.id === sidebarSecretId && row.value !== undefined
)} )}
modifyKey={listenChangeKey} modifyKey={listenChangeKey}
modifyValue={listenChangeValue} modifyValue={listenChangeValue}
+1 -1
View File
@@ -159,7 +159,7 @@ export default function SettingsOrg() {
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Head> </Head>
<div className="flex flex-row"> <div className="flex flex-row">
<div className="w-full max-h-screen pb-2 overflow-y-auto"> <div className="w-full max-h-screen pb-2">
<NavHeader pageName={t('settings-org:title')} /> <NavHeader pageName={t('settings-org:title')} />
<AddIncidentContactDialog <AddIncidentContactDialog
isOpen={isAddIncidentContactOpen} isOpen={isAddIncidentContactOpen}
@@ -76,7 +76,7 @@ export default function PersonalSettings() {
setApiKeys={setApiKeys} setApiKeys={setApiKeys}
/> />
<div className="flex flex-row"> <div className="flex flex-row">
<div className="w-full max-h-screen pb-2 overflow-y-auto"> <div className="w-full max-h-screen pb-2">
<NavHeader pageName={t('settings-personal:title')} isProjectRelated={false} /> <NavHeader pageName={t('settings-personal:title')} isProjectRelated={false} />
<div className="flex flex-row justify-between items-center ml-6 mt-8 mb-6 text-xl max-w-5xl"> <div className="flex flex-row justify-between items-center ml-6 mt-8 mb-6 text-xl max-w-5xl">
<div className="flex flex-col justify-start items-start text-3xl"> <div className="flex flex-col justify-start items-start text-3xl">