mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 18:28:27 +00:00
Merge remote-tracking branch 'origin/main' into auto-capitalization-toggle
This commit is contained in:
@@ -187,10 +187,10 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
});
|
||||
const userWorkspaces = orgUserProjects;
|
||||
if (
|
||||
userWorkspaces.length === 0 &&
|
||||
router.asPath !== '/noprojects' &&
|
||||
!router.asPath.includes('home') &&
|
||||
!router.asPath.includes('settings') ||
|
||||
(userWorkspaces.length === 0 &&
|
||||
router.asPath !== '/noprojects' &&
|
||||
!router.asPath.includes('home') &&
|
||||
!router.asPath.includes('settings')) ||
|
||||
router.asPath === '/dashboard/undefined'
|
||||
) {
|
||||
router.push('/noprojects');
|
||||
@@ -246,16 +246,15 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed w-full md:block flex flex-col h-screen">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.2.2/cdn.js" defer />
|
||||
<div className="flex h-screen w-full flex-col overflow-x-hidden">
|
||||
<NavBarDashboard />
|
||||
<div className="flex flex-col md:flex-row flex-1">
|
||||
<aside className="bg-bunker-600 border-r border-mineshaft-500 w-full md:w-60 h-screen">
|
||||
<nav className="flex flex-col justify-between items-between h-full">
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<aside className="w-full border-r border-mineshaft-500 bg-bunker-600 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between">
|
||||
{/* <div className="py-6"></div> */}
|
||||
<div>
|
||||
<div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4">
|
||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||
<div className="mt-6 mb-6 flex h-20 w-full flex-col items-center justify-center bg-bunker-600 px-4">
|
||||
<div className="ml-1 mb-1 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t('nav:menu.project')}
|
||||
</div>
|
||||
{Object.keys(workspaceMapping).length > 0 ? (
|
||||
@@ -279,29 +278,29 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
<ul>
|
||||
{Object.keys(workspaceMapping).length > 0 &&
|
||||
menuItems.map(({ href, title, emoji }) => (
|
||||
<li className="mt-0.5 mx-2" key={title}>
|
||||
<li className="mx-2 mt-0.5" key={title}>
|
||||
{router.asPath.split('/')[1] === href.split('/')[1] &&
|
||||
(['project', 'billing', 'org', 'personal'].includes(
|
||||
router.asPath.split('/')[2]
|
||||
)
|
||||
? router.asPath.split('/')[2] === href.split('/')[2]
|
||||
: true) ? (
|
||||
<div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
|
||||
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
|
||||
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
|
||||
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
|
||||
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
|
||||
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
{title}
|
||||
</div>
|
||||
) : router.asPath === '/noprojects' ? (
|
||||
<div className="flex p-2.5 text-white text-sm rounded">
|
||||
<p className="w-10 flex items-center justify-center text-lg">{emoji}</p>
|
||||
<div className="flex rounded p-2.5 text-sm text-white">
|
||||
<p className="flex w-10 items-center justify-center text-lg">{emoji}</p>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={href}>
|
||||
<div className="flex p-2.5 text-white text-sm rounded cursor-pointer hover:bg-primary-50/5">
|
||||
<p className="w-10 flex items-center justify-center text-lg">
|
||||
<div className="flex cursor-pointer rounded p-2.5 text-sm text-white hover:bg-primary-50/5">
|
||||
<p className="flex w-10 items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
{title}
|
||||
@@ -312,11 +311,11 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="w-full mt-40 mb-4 px-2">
|
||||
<div className="mt-40 mb-4 w-full px-2">
|
||||
{router.asPath.split('/')[1] === 'home' ? (
|
||||
<div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
|
||||
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
|
||||
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
|
||||
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
|
||||
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
|
||||
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
@@ -336,8 +335,8 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
</div>
|
||||
) : (
|
||||
<Link href={`/home/${workspaceMapping[workspaceSelected as any]}`}>
|
||||
<div className="relative flex p-2.5 overflow-visible text-white h-10 text-sm rounded cursor-pointer bg-white/10 hover:bg-primary-50/[0.15] mt-max">
|
||||
<p className="w-10 flex items-center justify-center text-lg">
|
||||
<div className="mt-max relative flex h-10 cursor-pointer overflow-visible rounded bg-white/10 p-2.5 text-sm text-white hover:bg-primary-50/[0.15]">
|
||||
<p className="flex w-10 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
@@ -369,12 +368,12 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
<main className="flex-1 bg-bunker-800">{children}</main>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:hidden bg-bunker-800 w-screen h-screen flex flex-col justify-center items-center">
|
||||
<FontAwesomeIcon icon={faMobile} className="text-gray-300 text-7xl mb-8" />
|
||||
<p className="text-gray-200 px-6 text-center text-lg max-w-sm">
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t('common:no-mobile')} `}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -42,15 +42,15 @@ const UpgradePlanModal = ({
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
>
|
||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker border border-bunker-400 p-6 pt-5 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker border border-mineshaft-500 p-6 pt-5 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-bunker-200'
|
||||
className='text-xl font-medium leading-6 text-primary'
|
||||
>
|
||||
Unleash Infisical's Full Power
|
||||
</Dialog.Title>
|
||||
<div className='mt-2'>
|
||||
<p className='text-sm text-bunker-300 mb-0.5'>
|
||||
<p className='text-sm text-bunker-300 mb-1'>
|
||||
{text}
|
||||
</p>
|
||||
<p className='text-sm text-bunker-300'>
|
||||
@@ -60,7 +60,7 @@ const UpgradePlanModal = ({
|
||||
<div className='mt-4'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex justify-center rounded-md border border-transparent bg-primary opacity-90 hover:opacity-100 px-4 py-2 text-sm font-medium text-black hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
className='inline-flex justify-center rounded-md border border-transparent bg-primary opacity-80 hover:opacity-100 px-4 py-2 text-sm font-medium text-black hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={() => router.push(`/settings/billing/${localStorage.getItem("projectData.id")}`)}
|
||||
>
|
||||
Upgrade Now
|
||||
|
||||
@@ -41,7 +41,6 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
const onEnvCreateCB = async (env: Env) => {
|
||||
try {
|
||||
await onCreateEnv(env);
|
||||
@@ -71,14 +70,14 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div className="flex flex-col w-full">
|
||||
<p className="text-xl font-semibold mb-3">Project Environments</p>
|
||||
<p className="text-base text-gray-400 mb-4">
|
||||
<div className="flex w-full flex-row justify-between">
|
||||
<div className="flex w-full flex-col">
|
||||
<p className="mb-3 text-xl font-semibold">Project Environments</p>
|
||||
<p className="mb-4 text-base text-gray-400">
|
||||
Choose which environments will show up in your dashboard like development, staging,
|
||||
production
|
||||
</p>
|
||||
<p className="text-sm mr-1 text-gray-500 self-start">
|
||||
<p className="mr-1 self-start text-sm text-gray-500">
|
||||
Note: the text in slugs shows how these environmant should be accessed in CLI.
|
||||
</p>
|
||||
</div>
|
||||
@@ -86,10 +85,10 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
<Button
|
||||
text="Add New Env"
|
||||
onButtonPressed={() => {
|
||||
if (plan !== plans.starter || host !== "https://app.infisical.com") {
|
||||
handlePopUpOpen('createUpdateEnv')
|
||||
if (plan !== plans.starter || host !== 'https://app.infisical.com') {
|
||||
handlePopUpOpen('createUpdateEnv');
|
||||
} else {
|
||||
handlePopUpOpen('upgradePlan')
|
||||
handlePopUpOpen('upgradePlan');
|
||||
}
|
||||
}}
|
||||
color="mineshaft"
|
||||
@@ -98,32 +97,30 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
|
||||
<table className="w-full my-1">
|
||||
<div className="table-container relative mb-6 mt-1 w-full rounded-md border border-mineshaft-700 bg-bunker">
|
||||
<div className="absolute h-12 w-full rounded-t-md bg-white/5" />
|
||||
<table className="my-1 w-full">
|
||||
<thead className="text-bunker-300">
|
||||
<tr>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">Name</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">Slug</th>
|
||||
<th className="pl-6 pt-2.5 pb-2 text-left">Name</th>
|
||||
<th className="pl-6 pt-2.5 pb-2 text-left">Slug</th>
|
||||
<th aria-label="buttons" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.length > 0 ? (
|
||||
data.map(({ name, slug }) => (
|
||||
<tr key={name} className="bg-bunker-800 hover:bg-bunker-800/5 duration-100">
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{name}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">{slug}</td>
|
||||
<td className="py-2 border-mineshaft-700 border-t flex justify-end">
|
||||
<div className="hover:opacity-100 duration-200 flex items-center mr-2">
|
||||
<tr key={name} className="bg-bunker-800 duration-100 hover:bg-bunker-800/5">
|
||||
<td className="border-t border-mineshaft-700 py-2 pl-6 text-gray-300">{name}</td>
|
||||
<td className="border-t border-mineshaft-700 py-2 pl-6 text-gray-300">{slug}</td>
|
||||
<td className="flex justify-end border-t border-mineshaft-700 py-2">
|
||||
<div className="mr-2 flex items-center duration-200 hover:opacity-100">
|
||||
<Button
|
||||
onButtonPressed={() => {
|
||||
if (plan !== plans.starter || host !== "https://app.infisical.com") {
|
||||
handlePopUpOpen('createUpdateEnv', { name, slug })
|
||||
if (plan !== plans.starter || host !== 'https://app.infisical.com') {
|
||||
handlePopUpOpen('createUpdateEnv', { name, slug });
|
||||
} else {
|
||||
handlePopUpOpen('upgradePlan')
|
||||
handlePopUpOpen('upgradePlan');
|
||||
}
|
||||
}}
|
||||
color="mineshaft"
|
||||
@@ -131,13 +128,13 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
icon={faPencil}
|
||||
/>
|
||||
</div>
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center mr-6">
|
||||
<div className="mr-6 flex items-center opacity-50 duration-200 hover:opacity-100">
|
||||
<Button
|
||||
onButtonPressed={() => {
|
||||
if (plan !== plans.starter || host !== "https://app.infisical.com") {
|
||||
handlePopUpOpen('deleteEnv', { name, slug })
|
||||
if (plan !== plans.starter || host !== 'https://app.infisical.com') {
|
||||
handlePopUpOpen('deleteEnv', { name, slug });
|
||||
} else {
|
||||
handlePopUpOpen('upgradePlan')
|
||||
handlePopUpOpen('upgradePlan');
|
||||
}
|
||||
}}
|
||||
color="red"
|
||||
@@ -150,8 +147,8 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="text-center pt-7 pb-4 text-bunker-400">
|
||||
No environmants found
|
||||
<td colSpan={4} className="pt-7 pb-4 text-center text-bunker-400">
|
||||
No environments found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -174,7 +171,7 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
|
||||
onCreateSubmit={onEnvCreateCB}
|
||||
onEditSubmit={onEnvUpdateCB}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onClose={() => handlePopUpClose('upgradePlan')}
|
||||
text="You can add custom environments if you switch to Infisical's Team plan."
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { plans } from 'public/data/frequentConstants';
|
||||
|
||||
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||
import { Select, SelectItem } from '@app/components/v2';
|
||||
import updateUserProjectPermission from '@app/ee/api/memberships/UpdateUserProjectPermission';
|
||||
import getOrganizationSubscriptions from '@app/pages/api/organization/GetOrgSubscription';
|
||||
import changeUserRoleInWorkspace from '@app/pages/api/workspace/changeUserRoleInWorkspace';
|
||||
import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
import getProjectInfo from '@app/pages/api/workspace/getProjectInfo';
|
||||
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||
|
||||
import { decryptAssymmetric, encryptAssymmetric } from '../../utilities/cryptography/crypto';
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
import Button from '../buttons/Button';
|
||||
import UpgradePlanModal from '../dialog/UpgradePlan';
|
||||
|
||||
// const roles = ['admin', 'user'];
|
||||
// TODO: Set type for this
|
||||
type Props = {
|
||||
userData: any[];
|
||||
changeData: (users: any[]) => void;
|
||||
myUser: string;
|
||||
filter: string;
|
||||
};
|
||||
|
||||
type EnvironmentProps = {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the component that shows the users of a certin project
|
||||
* #TODO: add the possibility of choosing and doing operations on multiple users.
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
|
||||
const [roleSelected, setRoleSelected] = useState(
|
||||
Array(userData?.length).fill(userData.map((user) => user.role))
|
||||
);
|
||||
const host = window.location.origin;
|
||||
const router = useRouter();
|
||||
const [myRole, setMyRole] = useState('member');
|
||||
const [currentPlan, setCurrentPlan] = useState('');
|
||||
const [workspaceEnvs, setWorkspaceEnvs] = useState<EnvironmentProps[]>([]);
|
||||
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
const workspaceId = router.query.id as string;
|
||||
// Delete the row in the table (e.g. a user)
|
||||
// #TODO: Add a pop-up that warns you that the user is going to be deleted.
|
||||
const handleDelete = (membershipId: string, index: number) => {
|
||||
// setUserIdToBeDeleted(userId);
|
||||
// onClick();
|
||||
deleteUserFromWorkspace(membershipId);
|
||||
changeData(userData.filter((v, i) => i !== index));
|
||||
setRoleSelected([
|
||||
...roleSelected.slice(0, index),
|
||||
...roleSelected.slice(index + 1, userData?.length)
|
||||
]);
|
||||
};
|
||||
|
||||
// Update the rold of a certain user
|
||||
const handleRoleUpdate = (index: number, e: string) => {
|
||||
changeUserRoleInWorkspace(userData[index].membershipId, e.toLowerCase());
|
||||
changeData([
|
||||
...userData.slice(0, index),
|
||||
...[
|
||||
{
|
||||
key: userData[index].key,
|
||||
firstName: userData[index].firstName,
|
||||
lastName: userData[index].lastName,
|
||||
email: userData[index].email,
|
||||
role: e.toLocaleLowerCase(),
|
||||
status: userData[index].status,
|
||||
userId: userData[index].userId,
|
||||
membershipId: userData[index].membershipId,
|
||||
publicKey: userData[index].publicKey,
|
||||
deniedPermissions: userData[index].deniedPermissions
|
||||
}
|
||||
],
|
||||
...userData.slice(index + 1, userData?.length)
|
||||
]);
|
||||
createNotification({
|
||||
text: `Successfully changed user role.`,
|
||||
type: 'success'
|
||||
});
|
||||
};
|
||||
|
||||
const handlePermissionUpdate = (index: number, val: string, membershipId: string, slug: string ) => {
|
||||
let denials: { ability: string; environmentSlug: string; }[];
|
||||
if (val === "Read Only") {
|
||||
denials = [{
|
||||
ability: "write",
|
||||
environmentSlug: slug
|
||||
}];
|
||||
} else if (val === "No Access") {
|
||||
denials = [{
|
||||
ability: "write",
|
||||
environmentSlug: slug
|
||||
}, {
|
||||
ability: "read",
|
||||
environmentSlug: slug
|
||||
}];
|
||||
} else {
|
||||
denials = [];
|
||||
}
|
||||
|
||||
if (currentPlan !== plans.professional && host === 'https://app.infisical.com') {
|
||||
setIsUpgradeModalOpen(true);
|
||||
} else {
|
||||
const allDenials = userData[index].deniedPermissions.filter((perm: { ability: string; environmentSlug: string; }) => perm.environmentSlug !== slug).concat(denials);
|
||||
updateUserProjectPermission({ membershipId, denials: allDenials});
|
||||
changeData([
|
||||
...userData.slice(0, index),
|
||||
...[
|
||||
{
|
||||
key: userData[index].key,
|
||||
firstName: userData[index].firstName,
|
||||
lastName: userData[index].lastName,
|
||||
email: userData[index].email,
|
||||
role: userData[index].role,
|
||||
status: userData[index].status,
|
||||
userId: userData[index].userId,
|
||||
membershipId: userData[index].membershipId,
|
||||
publicKey: userData[index].publicKey,
|
||||
deniedPermissions: allDenials
|
||||
}
|
||||
],
|
||||
...userData.slice(index + 1, userData?.length)
|
||||
]);
|
||||
createNotification({
|
||||
text: `Successfully changed user permissions.`,
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
|
||||
(async () => {
|
||||
const result = await getProjectInfo({ projectId: workspaceId });
|
||||
setWorkspaceEnvs(result.environments);
|
||||
|
||||
const orgId = localStorage.getItem('orgData.id') as string;
|
||||
const subscriptions = await getOrganizationSubscriptions({
|
||||
orgId
|
||||
});
|
||||
if (subscriptions) {
|
||||
setCurrentPlan(subscriptions.data[0].plan.product)
|
||||
}
|
||||
})();
|
||||
}, [userData, myUser]);
|
||||
|
||||
const grantAccess = async (id: string, publicKey: string) => {
|
||||
const result = await getLatestFileKey({ workspaceId });
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: result.latestKey.encryptedKey,
|
||||
nonce: result.latestKey.nonce,
|
||||
publicKey: result.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
uploadKeys(workspaceId, id, ciphertext, nonce);
|
||||
router.reload();
|
||||
};
|
||||
|
||||
const closeUpgradeModal = () => {
|
||||
setIsUpgradeModalOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<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" />
|
||||
<UpgradePlanModal
|
||||
isOpen={isUpgradeModalOpen}
|
||||
onClose={closeUpgradeModal}
|
||||
text="You can change user permissions if you switch to Infisical's Professional plan."
|
||||
/>
|
||||
<table className="w-full my-0.5">
|
||||
<thead className="text-gray-400 text-sm font-light">
|
||||
<tr>
|
||||
<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-6 pr-10 py-3.5">ROLE</th>
|
||||
{workspaceEnvs.map(env => (
|
||||
<th key={guidGenerator()} className="text-left pl-8 py-1 max-w-min break-normal">
|
||||
<span>{env.name.toUpperCase()}<br/></span>
|
||||
{/* <span>PERMISSION</span> */}
|
||||
</th>
|
||||
))}
|
||||
<th aria-label="buttons" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userData?.filter(
|
||||
(user) =>
|
||||
user.firstName?.toLowerCase().includes(filter) ||
|
||||
user.lastName?.toLowerCase().includes(filter) ||
|
||||
user.email?.toLowerCase().includes(filter)
|
||||
).length > 0 &&
|
||||
userData
|
||||
?.filter(
|
||||
(user) =>
|
||||
user.firstName?.toLowerCase().includes(filter) ||
|
||||
user.lastName?.toLowerCase().includes(filter) ||
|
||||
user.email?.toLowerCase().includes(filter)
|
||||
)
|
||||
.map((row, index) => (
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-700">
|
||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.firstName} {row.lastName}
|
||||
</td>
|
||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.email}
|
||||
</td>
|
||||
<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">
|
||||
<Select
|
||||
className="w-36"
|
||||
// open={isOpen}
|
||||
onValueChange={(e) => handleRoleUpdate(index, e)}
|
||||
value={row.role}
|
||||
disabled={myRole !== 'admin' || myUser === row.email}
|
||||
// onOpenChange={(open) => setIsOpen(open)}
|
||||
>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
</Select>
|
||||
{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">
|
||||
<Button
|
||||
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
|
||||
color="mineshaft"
|
||||
text="Grant Access"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
{workspaceEnvs.map((env) => <td key={guidGenerator()} className="pl-8 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
<Select
|
||||
className="w-36"
|
||||
// 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"
|
||||
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") ? "Read Only" : "Read & Write")
|
||||
}
|
||||
disabled={myRole !== 'admin'}
|
||||
// onOpenChange={(open) => setIsOpen(open)}
|
||||
>
|
||||
<SelectItem value="No Access">No Access</SelectItem>
|
||||
<SelectItem value="Read Only">Read Only</SelectItem>
|
||||
<SelectItem value="Read & Write">Read & Write</SelectItem>
|
||||
</Select>
|
||||
</td>)}
|
||||
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||
{myUser !== row.email &&
|
||||
// row.role !== "admin" &&
|
||||
myRole !== 'member' ? (
|
||||
<div className="opacity-50 hover:opacity-100 flex items-center mt-0.5">
|
||||
<Button
|
||||
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-9 h-9" />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectUsersTable;
|
||||
@@ -2,8 +2,8 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import changeUserRoleInOrganization from '@app/pages/api/organization/changeUserRoleInOrganization';
|
||||
import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization';
|
||||
import changeUserRoleInWorkspace from '@app/pages/api/workspace/changeUserRoleInWorkspace';
|
||||
import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||
@@ -55,9 +55,9 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
]);
|
||||
};
|
||||
|
||||
// Update the rold of a certain user
|
||||
// Update the role of a certain user
|
||||
const handleRoleUpdate = (index: number, e: string) => {
|
||||
changeUserRoleInWorkspace(userData[index].membershipId, e);
|
||||
changeUserRoleInOrganization(String(localStorage.getItem("orgData.id")), userData[index].membershipId, e);
|
||||
changeData([
|
||||
...userData.slice(0, index),
|
||||
...[
|
||||
@@ -110,14 +110,14 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||
<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" />
|
||||
<table className="w-full my-0.5">
|
||||
<thead className="text-gray-400 text-sm font-light">
|
||||
<tr>
|
||||
<th className="text-left pl-6 py-3.5">FIRST NAME</th>
|
||||
<th className="text-left pl-6 py-3.5">LAST NAME</th>
|
||||
<th className="text-left pl-6 py-3.5">EMAIL</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-6 pr-10 py-3.5">ROLE</th>
|
||||
<th aria-label="buttons" />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -136,35 +136,30 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
user.email?.toLowerCase().includes(filter)
|
||||
)
|
||||
.map((row, index) => (
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-800/5">
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.firstName}
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-700">
|
||||
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.firstName} {row.lastName}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.lastName}
|
||||
</td>
|
||||
<td className="pl-6 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.email}
|
||||
</td>
|
||||
<td className="flex flex-row justify-end pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||
<div className="justify-end mr-6 mx-2 w-full h-full flex flex-row items-center">
|
||||
{row.status === 'granted' &&
|
||||
<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">
|
||||
{row.status === 'accepted' &&
|
||||
((myRole === 'admin' && row.role !== 'owner') || myRole === 'owner') &&
|
||||
myUser !== row.email ? (
|
||||
(myUser !== row.email) ? (
|
||||
<Listbox
|
||||
isSelected={row.role}
|
||||
onChange={(e) => handleRoleUpdate(index, e)}
|
||||
data={
|
||||
myRole === 'owner' ? ['owner', 'admin', 'member'] : ['admin', 'member']
|
||||
}
|
||||
text="Role: "
|
||||
/>
|
||||
) : (
|
||||
row.status !== 'invited' &&
|
||||
row.status !== 'verified' && (
|
||||
<Listbox
|
||||
isSelected={row.role}
|
||||
text="Role: "
|
||||
onChange={() => {
|
||||
throw new Error('Function not implemented.');
|
||||
}}
|
||||
@@ -173,7 +168,7 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
)
|
||||
)}
|
||||
{(row.status === 'invited' || row.status === 'verified') && (
|
||||
<div className="w-full pl-9">
|
||||
<div className="w-full pr-20">
|
||||
<Button
|
||||
onButtonPressed={() => deleteMembershipAndResendInvite(row.email)}
|
||||
color="mineshaft"
|
||||
@@ -193,10 +188,12 @@ const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||
{myUser !== row.email &&
|
||||
// row.role !== "admin" &&
|
||||
myRole !== 'member' ? (
|
||||
<div className="opacity-50 hover:opacity-100 flex items-center">
|
||||
<div className="opacity-50 hover:opacity-100 flex items-center mt-0.5">
|
||||
<Button
|
||||
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
||||
color="red"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { SetStateAction, useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { WorkspaceEnv } from '@app/hooks/api/types';
|
||||
|
||||
import getSecretsForProject from '../utilities/secrets/getSecretsForProject';
|
||||
import { Modal, ModalContent } from '../v2';
|
||||
|
||||
interface Secrets {
|
||||
label: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
interface CompareSecretsModalProps {
|
||||
compareModal: boolean;
|
||||
setCompareModal: React.Dispatch<SetStateAction<boolean>>;
|
||||
selectedEnv: WorkspaceEnv;
|
||||
workspaceEnvs: WorkspaceEnv[];
|
||||
workspaceId: string;
|
||||
currentSecret: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
const CompareSecretsModal = ({
|
||||
compareModal,
|
||||
setCompareModal,
|
||||
selectedEnv,
|
||||
workspaceEnvs,
|
||||
workspaceId,
|
||||
currentSecret
|
||||
}: CompareSecretsModalProps) => {
|
||||
const [secrets, setSecrets] = useState<Secrets[]>([]);
|
||||
|
||||
const getEnvSecrets = async () => {
|
||||
const workspaceEnvironments = workspaceEnvs?.filter((env) => env !== selectedEnv);
|
||||
const newSecrets = await Promise.all(
|
||||
workspaceEnvironments.map(async (env) => {
|
||||
// #TODO: optimize this query somehow...
|
||||
const allSecrets = await getSecretsForProject({ env: env.slug, workspaceId });
|
||||
const secret =
|
||||
allSecrets.find((item) => item.key === currentSecret.key)?.value ?? 'Not found';
|
||||
return { label: env.name, secret };
|
||||
})
|
||||
);
|
||||
setSecrets([{ label: selectedEnv.name, secret: currentSecret.value }, ...newSecrets]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (compareModal) {
|
||||
(async () => {
|
||||
await getEnvSecrets();
|
||||
})();
|
||||
}
|
||||
}, [compareModal]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={compareModal} onOpenChange={setCompareModal}>
|
||||
<ModalContent title={currentSecret?.key} onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||
<div className="space-y-4">
|
||||
{secrets.length === 0 ? (
|
||||
<div className="flex items-center bg-bunker-900 justify-center h-full py-4 rounded-md">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
secrets.map((item) => (
|
||||
<div key={`${currentSecret.key}${item.label}`} className="space-y-0.5">
|
||||
<p className="text-sm text-bunker-300">{item.label}</p>
|
||||
<input
|
||||
defaultValue={item.secret}
|
||||
className="h-no-capture border border-mineshaft-500 text-md min-w-16 no-scrollbar::-webkit-scrollbar peer z-10 w-full rounded-md bg-bunker-800 px-2 py-1.5 font-mono text-gray-400 caret-white outline-none duration-200 no-scrollbar focus:ring-2 focus:ring-primary/50 "
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default CompareSecretsModal;
|
||||
@@ -6,10 +6,12 @@ import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import SecretVersionList from '@app/ee/components/SecretVersionList';
|
||||
import { WorkspaceEnv } from '@app/hooks/api/types';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Toggle from '../basic/Toggle';
|
||||
import CommentField from './CommentField';
|
||||
import CompareSecretsModal from './CompareSecretsModal';
|
||||
import DashboardInputField from './DashboardInputField';
|
||||
import { DeleteActionButton } from './DeleteActionButton';
|
||||
import GenerateSecretMenu from './GenerateSecretMenu';
|
||||
@@ -40,6 +42,9 @@ interface SideBarProps {
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
deleteRow: (props: DeleteRowFunctionProps) => void;
|
||||
workspaceEnvs: WorkspaceEnv[];
|
||||
selectedEnv: WorkspaceEnv;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,15 +68,19 @@ const SideBar = ({
|
||||
modifyComment,
|
||||
buttonReady,
|
||||
savePush,
|
||||
deleteRow
|
||||
deleteRow,
|
||||
workspaceEnvs,
|
||||
selectedEnv,
|
||||
workspaceId
|
||||
}: SideBarProps) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride !== undefined);
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data[0]?.valueOverride !== undefined);
|
||||
const [compareModal, setCompareModal] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between">
|
||||
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-96 right-0 z-40 shadow-xl flex flex-col justify-between">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Image
|
||||
@@ -171,20 +180,38 @@ const SideBar = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]">
|
||||
<Button
|
||||
text={String(t('common:save-changes'))}
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
<DeleteActionButton
|
||||
onSubmit={() =>
|
||||
deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
|
||||
}
|
||||
/>
|
||||
<div className="mt-full mt-4 mb-4 flex max-w-sm flex-col justify-start space-y-2 px-4">
|
||||
<div>
|
||||
<Button
|
||||
text="Compare secret across environments"
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
onButtonPressed={() => setCompareModal(true)}
|
||||
/>
|
||||
<CompareSecretsModal
|
||||
compareModal={compareModal}
|
||||
setCompareModal={setCompareModal}
|
||||
currentSecret={{ key: data[0]?.key, value: data[0]?.value }}
|
||||
workspaceEnvs={workspaceEnvs}
|
||||
selectedEnv={selectedEnv}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
text={String(t('common:save-changes'))}
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
<DeleteActionButton
|
||||
onSubmit={() =>
|
||||
deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function Navbar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-50">
|
||||
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-50">
|
||||
<div className="m-auto flex justify-start items-center mx-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex justify-center py-4">
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useRouter } from 'next/router';
|
||||
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { useWorkspace } from '@app/context';
|
||||
import getOrganization from '@app/pages/api/organization/GetOrg';
|
||||
import getProjectInfo from '@app/pages/api/workspace/getProjectInfo';
|
||||
|
||||
/**
|
||||
* This is the component at the top of almost every page.
|
||||
@@ -23,8 +23,9 @@ export default function NavHeader({
|
||||
isProjectRelated?: boolean;
|
||||
}): JSX.Element {
|
||||
const [orgName, setOrgName] = useState('');
|
||||
const [workspaceName, setWorkspaceName] = useState('');
|
||||
const router = useRouter();
|
||||
const projectId = String(router.query.id);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -33,29 +34,24 @@ export default function NavHeader({
|
||||
orgId: orgId || ''
|
||||
});
|
||||
setOrgName(org.name);
|
||||
|
||||
const workspace = await getProjectInfo({
|
||||
projectId: String(router.query.id)
|
||||
});
|
||||
setWorkspaceName(workspace.name);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [projectId]);
|
||||
|
||||
return (
|
||||
<div className="pt-20 ml-6 flex flex-row items-center">
|
||||
<div className="bg-primary-900 h-6 w-6 rounded-md flex items-center justify-center text-mineshaft-100 mr-2">
|
||||
<div className="ml-6 flex flex-row items-center pt-8">
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||
{orgName?.charAt(0)}
|
||||
</div>
|
||||
<div className="text-primary text-sm font-semibold">{orgName}</div>
|
||||
<div className="text-sm font-semibold text-primary">{orgName}</div>
|
||||
{isProjectRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
|
||||
<div className="font-semibold text-primary text-sm">{workspaceName}</div>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-sm font-semibold text-primary">{currentWorkspace?.name}</div>
|
||||
</>
|
||||
)}
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
|
||||
<div className="text-gray-400 text-sm">{pageName}</div>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
|
||||
<div className="text-sm text-gray-400">{pageName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ const POSTHOG_HOST =
|
||||
process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
|
||||
const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!;
|
||||
const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!;
|
||||
const {SITE_URL} = process.env
|
||||
|
||||
export {
|
||||
ENV,
|
||||
POSTHOG_API_KEY,
|
||||
POSTHOG_HOST,
|
||||
SITE_URL,
|
||||
STRIPE_PRODUCT_PRO,
|
||||
STRIPE_PRODUCT_STARTER
|
||||
};
|
||||
STRIPE_PRODUCT_STARTER};
|
||||
@@ -1,5 +1,7 @@
|
||||
import { jsPDF } from 'jspdf';
|
||||
|
||||
import { SITE_URL } from './config';
|
||||
|
||||
interface PDFProps {
|
||||
personalName: string;
|
||||
personalEmail: string;
|
||||
@@ -73,7 +75,7 @@ function generateBackupPDF({ personalName, personalEmail, generatedKey }: PDFPro
|
||||
doc.roundedRect(170, 488, 375, 35, 5, 5, 'F');
|
||||
doc.setTextColor(23, 23, 23);
|
||||
doc.setFontSize(14);
|
||||
doc.text('https://app.infisical.com/login', 180, 420);
|
||||
doc.text(`${SITE_URL}/login`, 180, 420);
|
||||
doc.text(personalEmail, 180, 465);
|
||||
doc.text(generatedKey, 180, 510);
|
||||
doc.text('Need help? Contact us at [email protected]', 32, 575);
|
||||
|
||||
@@ -29,8 +29,8 @@ interface SecretProps {
|
||||
|
||||
interface FunctionProps {
|
||||
env: string;
|
||||
setIsKeyAvailable: any;
|
||||
setData: any;
|
||||
setIsKeyAvailable?: any;
|
||||
setData?: any;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ const getSecretsForProject = async ({
|
||||
|
||||
const latestKey = await getLatestFileKey({ workspaceId });
|
||||
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
|
||||
setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true);
|
||||
if (typeof setIsKeyAvailable === 'function') {
|
||||
setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true);
|
||||
}
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
|
||||
@@ -131,7 +133,10 @@ const getSecretsForProject = async ({
|
||||
)[0]?.comment
|
||||
}));
|
||||
|
||||
setData(result);
|
||||
if (typeof setData === 'function') {
|
||||
setData(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.log('Something went wrong during accessing or decripting secrets.');
|
||||
|
||||
@@ -18,14 +18,15 @@ const buttonVariants = cva(
|
||||
'font-inter font-medium',
|
||||
'cursor-pointer',
|
||||
'inline-flex items-center justify-center',
|
||||
'relative'
|
||||
'relative',
|
||||
'whitespace-nowrap'
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
colorSchema: {
|
||||
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
|
||||
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
|
||||
danger: ['bg-red', 'text-white', 'border-red']
|
||||
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
|
||||
},
|
||||
variant: {
|
||||
solid: '',
|
||||
@@ -33,7 +34,7 @@ const buttonVariants = cva(
|
||||
plain: ''
|
||||
},
|
||||
isDisabled: {
|
||||
true: 'bg-opacity-70 cursor-not-allowed',
|
||||
true: 'bg-mineshaft opacity-40 cursor-not-allowed',
|
||||
false: ''
|
||||
},
|
||||
isFullWidth: {
|
||||
@@ -45,9 +46,9 @@ const buttonVariants = cva(
|
||||
false: ''
|
||||
},
|
||||
size: {
|
||||
xs: ['text-xs', 'py-1', 'px-2'],
|
||||
sm: ['text-sm', 'py-2', 'px-4'],
|
||||
md: ['text-md', 'py-2', 'px-6'],
|
||||
xs: ['text-xs', 'py-1', 'px-1'],
|
||||
sm: ['text-sm', 'py-2', 'px-2'],
|
||||
md: ['text-md', 'py-2', 'px-4'],
|
||||
lg: ['text-lg', 'py-2', 'px-8']
|
||||
}
|
||||
},
|
||||
@@ -75,7 +76,7 @@ const buttonVariants = cva(
|
||||
{
|
||||
colorSchema: 'secondary',
|
||||
variant: 'plain',
|
||||
className: 'text-mineshaft'
|
||||
className: 'text-mineshaft-300'
|
||||
},
|
||||
{
|
||||
colorSchema: 'danger',
|
||||
@@ -101,7 +102,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
children,
|
||||
isDisabled = false,
|
||||
className = '',
|
||||
size = 'md',
|
||||
size = 'sm',
|
||||
variant = 'solid',
|
||||
isFullWidth,
|
||||
isRounded = true,
|
||||
@@ -144,9 +145,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
)}
|
||||
<span
|
||||
className={twMerge(
|
||||
'transition-all shrink-0 cursor-pointer',
|
||||
'shrink-0 cursor-pointer transition-all',
|
||||
loadingToggleClass,
|
||||
size === 'xs' ? 'mr-1' : 'mr-2'
|
||||
leftIcon && 'ml-2',
|
||||
size === 'xs' ? 'mr-1' : 'mr-3'
|
||||
)}
|
||||
>
|
||||
{leftIcon}
|
||||
@@ -154,7 +156,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
<span className={twMerge('transition-all', loadingToggleClass)}>{children}</span>
|
||||
<span
|
||||
className={twMerge(
|
||||
'transition-all shrink-0 cursor-pointer',
|
||||
'shrink-0 cursor-pointer transition-all',
|
||||
loadingToggleClass,
|
||||
size === 'xs' ? 'ml-1' : 'ml-2'
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,7 @@ export type CardTitleProps = {
|
||||
};
|
||||
|
||||
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
|
||||
<div className={twMerge('p-6 pb-4 font-sans text-xl font-medium', className)}>
|
||||
<div className={twMerge('px-6 py-4 font-sans text-xl font-medium', className)}>
|
||||
{children}
|
||||
{subTitle && <p className="py-1 text-sm font-normal text-gray-400">{subTitle}</p>}
|
||||
</div>
|
||||
@@ -29,7 +29,7 @@ export type CardBodyProps = {
|
||||
};
|
||||
|
||||
export const CardBody = ({ children, className }: CardBodyProps) => (
|
||||
<div className={twMerge('p-6 pt-0', className)}>{children}</div>
|
||||
<div className={twMerge('px-6 pb-6 pt-0', className)}>{children}</div>
|
||||
);
|
||||
|
||||
export type CardProps = {
|
||||
@@ -47,7 +47,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
<div
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
'flex flex-col w-full font-inter text-gray-200 bg-mineshaft-700 shadow-md',
|
||||
'flex w-full flex-col bg-mineshaft-800 font-inter text-gray-200 shadow-md',
|
||||
isFullHeight && 'h-full',
|
||||
isRounded && 'rounded-md',
|
||||
isPlain && 'shadow-none',
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useToggle } from '@app/hooks';
|
||||
|
||||
import { Button } from '../Button';
|
||||
import { FormControl } from '../FormControl';
|
||||
import { Input } from '../Input';
|
||||
import { Modal, ModalContent } from '../Modal';
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
onChange?: (isOpen: boolean) => void;
|
||||
deleteKey: string;
|
||||
title: string;
|
||||
onDeleteApproved: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const DeleteActionModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onChange,
|
||||
deleteKey,
|
||||
onDeleteApproved,
|
||||
title
|
||||
}: Props): JSX.Element => {
|
||||
const [inputData, setInputData] = useState('');
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
|
||||
useEffect(() => {
|
||||
setInputData('');
|
||||
}, [isOpen]);
|
||||
|
||||
const onDelete = async () => {
|
||||
setIsLoading.on();
|
||||
try {
|
||||
await onDeleteApproved();
|
||||
} catch {
|
||||
setIsLoading.off();
|
||||
} finally {
|
||||
setIsLoading.off();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(isOpenState) => {
|
||||
setInputData('');
|
||||
if (onChange) onChange(isOpenState);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title={title}
|
||||
subTitle="This action is irreversible!!!"
|
||||
footerContent={
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
colorSchema="danger"
|
||||
isDisabled={!(deleteKey === inputData) || isLoading}
|
||||
onClick={onDelete}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<form>
|
||||
<FormControl
|
||||
label={
|
||||
<div className="pb-2 text-sm">
|
||||
Type <span className="font-bold">{deleteKey}</span> to delete the resource
|
||||
</div>
|
||||
}
|
||||
className="mb-4"
|
||||
>
|
||||
<Input value={inputData} onChange={(e) => setInputData(e.target.value)} />
|
||||
</FormControl>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { DeleteActionModal } from './DeleteActionModal';
|
||||
@@ -11,7 +11,7 @@ export type FormLabelProps = {
|
||||
};
|
||||
|
||||
export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => (
|
||||
<Label.Root className="text-mineshaft-300 text-sm font-medium block mb-1 ml-0.5" htmlFor={id}>
|
||||
<Label.Root className="mb-1 ml-0.5 block text-sm font-medium text-mineshaft-300" htmlFor={id}>
|
||||
{label}
|
||||
{isRequired && <span className="ml-1 text-red">*</span>}
|
||||
</Label.Root>
|
||||
@@ -25,7 +25,7 @@ export type FormHelperTextProps = {
|
||||
export const FormHelperText = ({ isError, text }: FormHelperTextProps) => (
|
||||
<div
|
||||
className={twMerge(
|
||||
'text-xs font-inter flex items-center opacity-90 text-mineshaft-300 mt-2',
|
||||
'mt-2 flex items-center font-inter text-xs text-mineshaft-300 opacity-90',
|
||||
isError && 'text-red-600'
|
||||
)}
|
||||
>
|
||||
@@ -46,6 +46,7 @@ export type FormControlProps = {
|
||||
helperText?: ReactNode;
|
||||
errorText?: ReactNode;
|
||||
children: JSX.Element;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const FormControl = ({
|
||||
@@ -55,10 +56,11 @@ export const FormControl = ({
|
||||
helperText,
|
||||
errorText,
|
||||
id,
|
||||
isError
|
||||
isError,
|
||||
className
|
||||
}: FormControlProps): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
<div className={twMerge('mb-4', className)}>
|
||||
{typeof label === 'string' ? (
|
||||
<FormLabel label={label} isRequired={isRequired} id={id} />
|
||||
) : (
|
||||
|
||||
@@ -14,7 +14,7 @@ const iconButtonVariants = cva(
|
||||
[
|
||||
'button',
|
||||
'transition-all',
|
||||
'font-inter font-medium',
|
||||
'font-inter font-medium user-select-none',
|
||||
'cursor-pointer',
|
||||
'inline-flex items-center justify-center',
|
||||
'relative'
|
||||
@@ -22,9 +22,9 @@ const iconButtonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
colorSchema: {
|
||||
primary: ['bg-primary', 'text-black', 'border-primary hover:bg-opacity-80'],
|
||||
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
|
||||
danger: ['bg-red', 'text-white', 'border-red']
|
||||
primary: ['bg-primary', 'text-black', 'border-primary hover:opacity-80'],
|
||||
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-bunker-400'],
|
||||
danger: ['bg-[#973939] hover:bg-red', 'text-white', 'border-red']
|
||||
},
|
||||
variant: {
|
||||
solid: '',
|
||||
@@ -40,10 +40,10 @@ const iconButtonVariants = cva(
|
||||
false: ''
|
||||
},
|
||||
size: {
|
||||
xs: ['text-xs', 'py-1', 'px-2'],
|
||||
sm: ['text-sm', 'py-2', 'px-3'],
|
||||
md: ['text-md', 'py-3', 'px-4'],
|
||||
lg: ['text-lg', 'py-3', 'px-6']
|
||||
xs: ['text-xs', 'py-2', 'px-2'],
|
||||
sm: ['text-sm', 'py-3', 'px-3'],
|
||||
md: ['text-md', 'py-4', 'px-4'],
|
||||
lg: ['text-lg', 'py-6', 'px-6']
|
||||
}
|
||||
},
|
||||
compoundVariants: [
|
||||
@@ -97,7 +97,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
ariaLabel,
|
||||
isDisabled = false,
|
||||
className,
|
||||
size = 'md',
|
||||
size = 'sm',
|
||||
variant = 'solid',
|
||||
isRounded = true,
|
||||
colorSchema = 'primary',
|
||||
|
||||
@@ -8,10 +8,11 @@ type Props = {
|
||||
isRequired?: boolean;
|
||||
leftIcon?: ReactNode;
|
||||
rightIcon?: ReactNode;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const inputVariants = cva(
|
||||
'input w-full py-2 text-gray-400 placeholder-gray-500 placeholder-opacity-50',
|
||||
'input w-full py-2 text-gray-400 placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
@@ -46,7 +47,7 @@ const inputParentContainerVariants = cva('inline-flex font-inter items-center bo
|
||||
},
|
||||
isError: {
|
||||
true: 'border-red',
|
||||
false: 'border-mineshaft-400'
|
||||
false: 'border-mineshaft-500'
|
||||
},
|
||||
isFullWidth: {
|
||||
true: 'w-full',
|
||||
@@ -70,6 +71,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
className,
|
||||
isRounded = true,
|
||||
isFullWidth = true,
|
||||
isDisabled,
|
||||
isError = false,
|
||||
isRequired,
|
||||
leftIcon,
|
||||
@@ -87,9 +89,10 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
{...props}
|
||||
required={isRequired}
|
||||
ref={ref}
|
||||
disabled={isDisabled}
|
||||
className={twMerge(
|
||||
leftIcon ? 'pl-9' : 'pl-4',
|
||||
rightIcon ? 'pr-9' : 'pr-4',
|
||||
leftIcon ? 'pl-9' : 'pl-2.5',
|
||||
rightIcon ? 'pr-9' : 'pr-2.5',
|
||||
inputVariants({ className, isError, size, isRounded, variant })
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -11,33 +11,34 @@ export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
||||
title?: ReactNode;
|
||||
subTitle?: string;
|
||||
footerContent?: ReactNode;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
({ children, title, subTitle, className, footerContent, ...props }, forwardedRef) => (
|
||||
({ children, title, subTitle, className, footerContent, onClose, ...props }, forwardedRef) => (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay
|
||||
className="fixed inset-0 w-full h-full animate-fadeIn"
|
||||
style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}
|
||||
className="fixed inset-0 h-full w-full animate-fadeIn"
|
||||
style={{ backgroundColor: 'rgba(0, 0, 0, 0.7)' }}
|
||||
/>
|
||||
<DialogPrimitive.Content {...props} ref={forwardedRef}>
|
||||
<Card
|
||||
isRounded
|
||||
className={twMerge(
|
||||
'fixed max-w-md animate-popIn top-1/2 left-1/2 -translate-y-2/4 -translate-x-2/4',
|
||||
'fixed top-1/2 left-1/2 max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-50',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
|
||||
<CardBody>{children}</CardBody>
|
||||
{footerContent && <CardFooter>{footerContent}</CardFooter>}
|
||||
<DialogPrimitive.Close aria-label="Close" asChild>
|
||||
<DialogPrimitive.Close aria-label="Close" asChild onClick={onClose}>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="close"
|
||||
className="absolute top-2.5 right-2.5 text-white hover:bg-gray-600 rounded"
|
||||
className="absolute top-3 right-3 rounded text-white hover:bg-gray-600"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTimes} size="sm" className="cursor-pointer" />
|
||||
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
|
||||
</IconButton>
|
||||
</DialogPrimitive.Close>
|
||||
</Card>
|
||||
@@ -55,3 +56,6 @@ export const Modal = ({ isOpen, ...props }: ModalProps) => (
|
||||
|
||||
export const ModalTrigger = DialogPrimitive.Trigger;
|
||||
export type ModalTriggerProps = DialogPrimitive.DialogTriggerProps;
|
||||
|
||||
export const ModalClose = DialogPrimitive.Close;
|
||||
export type ModalCloseProps = DialogPrimitive.DialogCloseProps;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export type { ModalContentProps, ModalProps, ModalTriggerProps } from './Modal';
|
||||
export { Modal, ModalContent, ModalTrigger } from './Modal';
|
||||
export type { ModalCloseProps, ModalContentProps, ModalProps, ModalTriggerProps } from './Modal';
|
||||
export { Modal, ModalClose, ModalContent, ModalTrigger } from './Modal';
|
||||
|
||||
@@ -10,39 +10,45 @@ type Props = {
|
||||
children: ReactNode;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
dropdownContainerClassName?: string;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export type SelectProps = SelectPrimitive.SelectProps & Props;
|
||||
|
||||
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
({ children, placeholder, className, isLoading, ...props }, ref): JSX.Element => {
|
||||
(
|
||||
{ children, placeholder, className, isLoading, dropdownContainerClassName, ...props },
|
||||
ref
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<SelectPrimitive.Root {...props}>
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
`inline-flex items-center justify-between data-[placeholder]:text-gray-500
|
||||
px-4 py-2.5 font-inter text-sm text-white rounded-md bg-mineshaft-800`,
|
||||
`inline-flex items-center justify-between rounded-md
|
||||
bg-bunker-800 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-gray-500`,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<SelectPrimitive.Value placeholder={placeholder} />
|
||||
<SelectPrimitive.Icon className="ml-3">
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||
</SelectPrimitive.Icon>
|
||||
{!props.disabled && (
|
||||
<SelectPrimitive.Icon className="ml-3">
|
||||
<FontAwesomeIcon icon={faChevronDown} size="sm" />
|
||||
</SelectPrimitive.Icon>
|
||||
)}
|
||||
</SelectPrimitive.Trigger>
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
position="popper"
|
||||
sideOffset={5}
|
||||
className="overflow-hidden text-white rounded-md shadow-md font-inter bg-mineshaft-800"
|
||||
style={{ width: 'var(--radix-select-trigger-width)' }}
|
||||
className={twMerge(
|
||||
'relative left-4 top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md',
|
||||
dropdownContainerClassName
|
||||
)}
|
||||
>
|
||||
<SelectPrimitive.ScrollUpButton>
|
||||
<FontAwesomeIcon icon={faChevronUp} size="sm" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<SelectPrimitive.Viewport className="p-2">
|
||||
<SelectPrimitive.Viewport className="p-1.5">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<Spinner size="xs" />
|
||||
@@ -75,16 +81,16 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||
<SelectPrimitive.Item
|
||||
{...props}
|
||||
className={twMerge(
|
||||
`text-sm rounded-sm transition-all hover:text-primary
|
||||
hover:bg-mineshaft-700 flex items-center pl-10 pr-4 py-2 cursor-pointer
|
||||
select-none outline-none relative`,
|
||||
isSelected && 'text-primary',
|
||||
isDisabled && 'text-gray-600 hover:bg-transparent cursor-not-allowed hover:text-gray-600',
|
||||
`relative flex cursor-pointer
|
||||
select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
|
||||
outline-none transition-all hover:bg-mineshaft-500`,
|
||||
isSelected && 'bg-primary',
|
||||
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-gray-600',
|
||||
className
|
||||
)}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator className="absolute left-2">
|
||||
<SelectPrimitive.ItemIndicator className="absolute left-3.5">
|
||||
<FontAwesomeIcon icon={faCheck} size="sm" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
<SelectPrimitive.ItemText className="">{children}</SelectPrimitive.ItemText>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { HTMLAttributes, ReactNode, TdHTMLAttributes } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export type TableContainerProps = {
|
||||
@@ -14,7 +14,7 @@ export const TableContainer = ({
|
||||
}: TableContainerProps): JSX.Element => (
|
||||
<div
|
||||
className={twMerge(
|
||||
'overflow-x-auto font-inter shadow-md relative border border-solid border-mineshaft-700',
|
||||
'relative w-full overflow-x-auto border border-solid border-mineshaft-700 font-inter shadow-md',
|
||||
isRounded && 'rounded-md',
|
||||
className
|
||||
)}
|
||||
@@ -32,7 +32,7 @@ export type TableProps = {
|
||||
export const Table = ({ children, className }: TableProps): JSX.Element => (
|
||||
<table
|
||||
className={twMerge(
|
||||
'bg-bunker-800 p-2 roun text-gray-300 w-full text-sm text-left rounded-md',
|
||||
'w-full rounded rounded-md bg-bunker-800 p-2 text-left text-sm text-gray-300',
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -47,7 +47,7 @@ export type THeadProps = {
|
||||
};
|
||||
|
||||
export const THead = ({ children, className }: THeadProps): JSX.Element => (
|
||||
<thead className={twMerge('text-xs bg-bunker text-bunker-300 uppercase', className)}>
|
||||
<thead className={twMerge('bg-bunker text-xs uppercase text-bunker-300', className)}>
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
@@ -56,20 +56,22 @@ export const THead = ({ children, className }: THeadProps): JSX.Element => (
|
||||
export type TrProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
} & HTMLAttributes<HTMLTableRowElement>;
|
||||
|
||||
export const Tr = ({ children, className }: TrProps): JSX.Element => (
|
||||
<tr className={twMerge('border border-solid border-mineshaft-700', className)}>{children}</tr>
|
||||
export const Tr = ({ children, className, ...props }: TrProps): JSX.Element => (
|
||||
<tr className={twMerge('border border-solid border-mineshaft-700 hover:bg-bunker-700', className)} {...props}>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
|
||||
// table head columns
|
||||
export type ThProps = {
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Th = ({ children, className }: ThProps): JSX.Element => (
|
||||
<th className={twMerge('px-6 py-3 font-medium', className)}>{children}</th>
|
||||
<th className={twMerge('px-5 pt-4 pb-3.5 font-medium font-semibold bg-bunker-500', className)}>{children}</th>
|
||||
);
|
||||
|
||||
// table body
|
||||
@@ -84,10 +86,12 @@ export const TBody = ({ children, className }: TBodyProps): JSX.Element => (
|
||||
|
||||
// table body columns
|
||||
export type TdProps = {
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
} & TdHTMLAttributes<HTMLTableCellElement>;
|
||||
|
||||
export const Td = ({ children, className }: TdProps): JSX.Element => (
|
||||
<td className={twMerge('text-left px-6 py-3', className)}>{children}</td>
|
||||
export const Td = ({ children, className, ...props }: TdProps): JSX.Element => (
|
||||
<td className={twMerge('px-5 py-2 text-left', className)} {...props}>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Button } from '../Button';
|
||||
import { Modal, ModalClose, ModalContent } from '../Modal';
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Unleash Infisical's Full Power"
|
||||
footerContent={[
|
||||
<Link
|
||||
href={`/settings/billing/${localStorage.getItem('projectData.id') as string}`}
|
||||
key="upgrade-plan"
|
||||
>
|
||||
<Button className="mr-4">Upgrade Plan</Button>
|
||||
</Link>,
|
||||
<ModalClose asChild key="upgrade-plan-cancel">
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
]}
|
||||
>
|
||||
<p className="mb-4 text-bunker-300">{text}</p>
|
||||
<p className="font-medium text-bunker-300">
|
||||
Upgrade and get access to this, as well as to other powerful enhancements.
|
||||
</p>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { UpgradePlanModal } from './UpgradePlanModal';
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './Button';
|
||||
export * from './Card';
|
||||
export * from './Checkbox';
|
||||
export * from './DeleteActionModal';
|
||||
export * from './Dropdown';
|
||||
export * from './FormControl';
|
||||
export * from './IconButton';
|
||||
@@ -12,3 +13,4 @@ export * from './Spinner';
|
||||
export * from './Switch';
|
||||
export * from './Table';
|
||||
export * from './TextArea';
|
||||
export * from './UpgradePlanModal';
|
||||
|
||||
+25
-3
@@ -15,10 +15,32 @@ export const publicPaths = [
|
||||
`/privacy`,
|
||||
`/terms`,
|
||||
`/subprocessors`,
|
||||
`/verify-email`,
|
||||
`/password-reset`
|
||||
];
|
||||
|
||||
export const languageMap = {
|
||||
en: "English",
|
||||
ko: "한국어",
|
||||
fr: "Français",
|
||||
en: 'English',
|
||||
ko: '한국어',
|
||||
fr: 'Français'
|
||||
};
|
||||
|
||||
interface Mapping {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const plansDev: Mapping = {
|
||||
starter: 'prod_Mb4ATFT5QAHoPM',
|
||||
team: 'prod_NEpD2WMXUS2eDn',
|
||||
professional: 'prod_Mb4CetZ2jE7jdl',
|
||||
enterprise: 'licence_key_required'
|
||||
};
|
||||
|
||||
const plansProd: Mapping = {
|
||||
starter: 'prod_Mb8oR5XNwyFTul',
|
||||
team: 'prod_NEp7fAB3UJWK6A',
|
||||
professional: 'prod_Mb8pUIpA0OUi5N',
|
||||
enterprise: 'licence_key_required'
|
||||
};
|
||||
|
||||
export const plans = plansProd || plansDev;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { createContext, ReactNode, useContext, useMemo } from 'react';
|
||||
|
||||
import { useGetOrgSubscription } from '@app/hooks/api';
|
||||
import { GetSubscriptionPlan } from '@app/hooks/api/types';
|
||||
|
||||
import { useWorkspace } from '../WorkspaceContext';
|
||||
// import { Subscription } from '@app/hooks/api/workspace/types';
|
||||
|
||||
type TSubscriptionContext = {
|
||||
subscription?: GetSubscriptionPlan;
|
||||
subscriptionPlan: string;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const SubscriptionContext = createContext<TSubscriptionContext | null>(null);
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const SubscriptionProvider = ({ children }: Props): JSX.Element => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useGetOrgSubscription({
|
||||
orgID: currentWorkspace?.organization || ''
|
||||
});
|
||||
|
||||
// memorize the workspace details for the context
|
||||
const value = useMemo<TSubscriptionContext>(
|
||||
() => ({
|
||||
subscription: data,
|
||||
subscriptionPlan: data?.data?.[0]?.plan?.product || '',
|
||||
isLoading
|
||||
}),
|
||||
[data, isLoading]
|
||||
);
|
||||
|
||||
return <SubscriptionContext.Provider value={value}>{children}</SubscriptionContext.Provider>;
|
||||
};
|
||||
|
||||
export const useSubscription = () => {
|
||||
const ctx = useContext(SubscriptionContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useSubscription has to be used within <SubscriptionContext.Provider>');
|
||||
}
|
||||
|
||||
return ctx;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createContext, ReactNode, useContext, useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { useGetUserWorkspaces } from '@app/hooks/api';
|
||||
import { Workspace } from '@app/hooks/api/workspace/types';
|
||||
|
||||
type TWorkspaceContext = {
|
||||
workspaces: Workspace[];
|
||||
currentWorkspace?: Workspace;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const WorkspaceContext = createContext<TWorkspaceContext | null>(null);
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
|
||||
const { data: ws, isLoading } = useGetUserWorkspaces();
|
||||
const router = useRouter();
|
||||
const workspaceId = router.query.id;
|
||||
|
||||
// memorize the workspace details for the context
|
||||
const value = useMemo<TWorkspaceContext>(() => {
|
||||
return {
|
||||
workspaces: ws || [],
|
||||
currentWorkspace: (ws || []).find(({ _id: id }) => id === workspaceId),
|
||||
isLoading
|
||||
};
|
||||
}, [ws, workspaceId, isLoading]);
|
||||
|
||||
return <WorkspaceContext.Provider value={value}>{children}</WorkspaceContext.Provider>;
|
||||
};
|
||||
|
||||
export const useWorkspace = () => {
|
||||
const ctx = useContext(WorkspaceContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useWorkspace has to be used within <WorkspaceContext.Provider>');
|
||||
}
|
||||
|
||||
return ctx;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';
|
||||
@@ -0,0 +1,2 @@
|
||||
export { SubscriptionProvider, useSubscription } from './SubscriptionContext';
|
||||
export { useWorkspace, WorkspaceProvider } from './WorkspaceContext';
|
||||
@@ -0,0 +1,39 @@
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This function updates user permissions for a certain environment in a project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.membershipId - membershipId of a certain user in a project
|
||||
* @param {*[]} obj.denials - permissions that we are prohibitting users to do
|
||||
* @returns
|
||||
*/
|
||||
const updateUserProjectPermission = async ({
|
||||
membershipId,
|
||||
denials
|
||||
}: {
|
||||
membershipId: string;
|
||||
denials: {
|
||||
ability: string;
|
||||
environmentSlug: string;
|
||||
}[]
|
||||
}) =>
|
||||
SecurityClient.fetchCall(`/api/v1/membership/${membershipId}/deny-permissions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
permissions: denials
|
||||
})
|
||||
}).then(async (res) => {
|
||||
console.log({
|
||||
permissions: denials
|
||||
}, res)
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log('Failed to update user permissions for a certain environment in a project');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default updateUserProjectPermission;
|
||||
@@ -1 +1,5 @@
|
||||
export { useGetAuthToken } from './auth';
|
||||
export * from './auth';
|
||||
export * from './keys';
|
||||
export * from './serviceTokens';
|
||||
export * from './subscriptions';
|
||||
export * from './workspace';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { useGetUserWsKey } from './queries';
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { apiRequest } from '@app/config/request';
|
||||
|
||||
import { UserWsKeyPair } from './types';
|
||||
|
||||
const encKeyKeys = {
|
||||
getUserWorkspaceKey: (workspaceID: string) => ['worksapce-key-pair', { workspaceID }] as const
|
||||
};
|
||||
|
||||
const fetchUserWsKey = async (workspaceID: string) => {
|
||||
const { data } = await apiRequest.get<{ latestKey: UserWsKeyPair }>(
|
||||
`/api/v1/key/${workspaceID}/latest`
|
||||
);
|
||||
|
||||
return data.latestKey;
|
||||
};
|
||||
|
||||
export const useGetUserWsKey = (workspaceID: string) =>
|
||||
useQuery({
|
||||
queryKey: encKeyKeys.getUserWorkspaceKey(workspaceID),
|
||||
queryFn: () => fetchUserWsKey(workspaceID),
|
||||
enabled: Boolean(workspaceID)
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
export type UserWsKeyPair = {
|
||||
_id: string;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
sender: Sender;
|
||||
receiver: string;
|
||||
workspace: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
__v: number;
|
||||
};
|
||||
|
||||
export type Sender = {
|
||||
_id: string;
|
||||
email: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
__v: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
publicKey: string;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { useCreateServiceToken, useDeleteServiceToken, useGetUserWsServiceTokens } from './queries';
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { apiRequest } from '@app/config/request';
|
||||
|
||||
import {
|
||||
CreateServiceTokenDTO,
|
||||
CreateServiceTokenRes,
|
||||
DeleteServiceTokenRes,
|
||||
ServiceToken
|
||||
} from './types';
|
||||
|
||||
const serviceTokenKeys = {
|
||||
getAllWorkspaceServiceToken: (workspaceID: string) => [{ workspaceID }, 'service-tokens'] as const
|
||||
};
|
||||
|
||||
const fetchWorkspaceServiceTokens = async (workspaceID: string) => {
|
||||
const { data } = await apiRequest.get<{ serviceTokenData: ServiceToken[] }>(
|
||||
`/api/v2/workspace/${workspaceID}/service-token-data`
|
||||
);
|
||||
|
||||
return data.serviceTokenData;
|
||||
};
|
||||
|
||||
type UseGetWorkspaceServiceTokensProps = { workspaceID: string };
|
||||
|
||||
export const useGetUserWsServiceTokens = ({ workspaceID }: UseGetWorkspaceServiceTokensProps) =>
|
||||
useQuery({
|
||||
queryKey: serviceTokenKeys.getAllWorkspaceServiceToken(workspaceID),
|
||||
queryFn: () => fetchWorkspaceServiceTokens(workspaceID),
|
||||
enabled: Boolean(workspaceID)
|
||||
});
|
||||
|
||||
// mutation
|
||||
export const useCreateServiceToken = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<CreateServiceTokenRes, {}, CreateServiceTokenDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post('/api/v2/service-token/', body);
|
||||
return data;
|
||||
},
|
||||
onSuccess: ({ serviceTokenData: { workspace } }) => {
|
||||
queryClient.invalidateQueries(serviceTokenKeys.getAllWorkspaceServiceToken(workspace));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteServiceToken = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<DeleteServiceTokenRes, {}, string>({
|
||||
mutationFn: async (serviceTokenId) => {
|
||||
const { data } = await apiRequest.delete(`/api/v2/service-token/${serviceTokenId}`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: ({ serviceTokenData: { workspace } }) => {
|
||||
queryClient.invalidateQueries(serviceTokenKeys.getAllWorkspaceServiceToken(workspace));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
export type ServiceToken = {
|
||||
_id: string;
|
||||
name: string;
|
||||
workspace: string;
|
||||
environment: string;
|
||||
user: string;
|
||||
expiresAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
__v: number;
|
||||
};
|
||||
|
||||
export type CreateServiceTokenDTO = {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
expiresIn: number;
|
||||
encryptedKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
export type CreateServiceTokenRes = {
|
||||
serviceToken: string;
|
||||
serviceTokenData: ServiceToken;
|
||||
};
|
||||
|
||||
export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken };
|
||||
@@ -0,0 +1 @@
|
||||
export { useGetOrgSubscription } from './queries';
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { apiRequest } from '@app/config/request';
|
||||
|
||||
import { GetSubscriptionPlan } from './types';
|
||||
|
||||
// import { Workspace } from './types';
|
||||
|
||||
const subscriptionKeys = {
|
||||
getOrgSubsription: (orgID: string) => ['subscription', { orgID }] as const
|
||||
};
|
||||
|
||||
const fetchOrgSubscription = async (orgID: string) => {
|
||||
const { data } = await apiRequest.get<{ subscriptions: GetSubscriptionPlan }>(
|
||||
`/api/v1/organization/${orgID}/subscriptions`
|
||||
);
|
||||
|
||||
return data.subscriptions;
|
||||
};
|
||||
|
||||
type UseGetOrgSubscriptionProps = {
|
||||
orgID: string;
|
||||
};
|
||||
|
||||
export const useGetOrgSubscription = ({ orgID }: UseGetOrgSubscriptionProps) =>
|
||||
useQuery({
|
||||
queryKey: subscriptionKeys.getOrgSubsription(orgID),
|
||||
queryFn: () => fetchOrgSubscription(orgID),
|
||||
enabled: Boolean(orgID)
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
export type GetSubscriptionPlan = {
|
||||
data: { plan: SubscriptionPlan }[];
|
||||
};
|
||||
|
||||
export type SubscriptionPlan = {
|
||||
id: string;
|
||||
object: string;
|
||||
active: boolean;
|
||||
aggregate_usage: unknown;
|
||||
amount: 1400;
|
||||
amount_decimal: 1400;
|
||||
billing_scheme: string;
|
||||
created: 1674833546;
|
||||
currency: string;
|
||||
interval: string;
|
||||
interval_count: 1;
|
||||
livemode: false;
|
||||
metadata: {};
|
||||
nickname: null;
|
||||
product: string;
|
||||
tiers_mode: unknown;
|
||||
transform_usage: unknown;
|
||||
trial_period_days: unknown;
|
||||
usage_type: string;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export type { GetAuthTokenAPI } from './auth/types';
|
||||
export type { UserWsKeyPair } from './keys/types';
|
||||
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
|
||||
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
|
||||
export type {
|
||||
CreateEnvironmentDTO,
|
||||
DeleteEnvironmentDTO,
|
||||
DeleteWorkspaceDTO,
|
||||
RenameWorkspaceDTO,
|
||||
UpdateEnvironmentDTO,
|
||||
Workspace,
|
||||
WorkspaceEnv
|
||||
} from './workspace/types';
|
||||
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
useCreateWsEnvironment,
|
||||
useDeleteWorkspace,
|
||||
useDeleteWsEnvironment,
|
||||
useGetUserWorkspaces,
|
||||
useRenameWorkspace,
|
||||
useUpdateWsEnvironment
|
||||
} from './queries';
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { apiRequest } from '@app/config/request';
|
||||
|
||||
import {
|
||||
CreateEnvironmentDTO,
|
||||
DeleteEnvironmentDTO,
|
||||
DeleteWorkspaceDTO,
|
||||
RenameWorkspaceDTO,
|
||||
UpdateEnvironmentDTO,
|
||||
Workspace
|
||||
} from './types';
|
||||
|
||||
const workspaceKeys = {
|
||||
getAllUserWorkspace: ['workspaces'] as const
|
||||
};
|
||||
|
||||
const fetchUserWorkspaces = async () => {
|
||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
|
||||
|
||||
return data.workspaces;
|
||||
};
|
||||
|
||||
export const useGetUserWorkspaces = () =>
|
||||
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
|
||||
|
||||
// mutation
|
||||
export const useRenameWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, RenameWorkspaceDTO>({
|
||||
mutationFn: ({ workspaceID, newWorkspaceName }) =>
|
||||
apiRequest.post(`/api/v1/workspace/${workspaceID}/name`, { name: newWorkspaceName }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, DeleteWorkspaceDTO>({
|
||||
mutationFn: ({ workspaceID }) => apiRequest.delete(`/api/v1/workspace/${workspaceID}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateWsEnvironment = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, CreateEnvironmentDTO>({
|
||||
mutationFn: ({ workspaceID, environmentName, environmentSlug }) =>
|
||||
apiRequest.post(`/api/v2/workspace/${workspaceID}/environments`, {
|
||||
environmentName,
|
||||
environmentSlug
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateWsEnvironment = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, UpdateEnvironmentDTO>({
|
||||
mutationFn: ({ workspaceID, environmentName, environmentSlug, oldEnvironmentSlug }) =>
|
||||
apiRequest.put(`/api/v2/workspace/${workspaceID}/environments`, {
|
||||
environmentName,
|
||||
environmentSlug,
|
||||
oldEnvironmentSlug
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteWsEnvironment = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, DeleteEnvironmentDTO>({
|
||||
mutationFn: ({ workspaceID, environmentSlug }) =>
|
||||
apiRequest.delete(`/api/v2/workspace/${workspaceID}/environments`, {
|
||||
data: { environmentSlug }
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
export type Workspace = {
|
||||
__v: number;
|
||||
_id: string;
|
||||
name: string;
|
||||
organization: string;
|
||||
environments: WorkspaceEnv[];
|
||||
};
|
||||
|
||||
export type WorkspaceEnv = { name: string; slug: string };
|
||||
|
||||
// mutation dto
|
||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
||||
|
||||
export type DeleteWorkspaceDTO = { workspaceID: string };
|
||||
|
||||
export type CreateEnvironmentDTO = {
|
||||
workspaceID: string;
|
||||
environmentSlug: string;
|
||||
environmentName: string;
|
||||
};
|
||||
|
||||
export type UpdateEnvironmentDTO = {
|
||||
workspaceID: string;
|
||||
oldEnvironmentSlug: string;
|
||||
environmentSlug: string;
|
||||
environmentName: string;
|
||||
};
|
||||
|
||||
export type DeleteEnvironmentDTO = { workspaceID: string; environmentSlug: string };
|
||||
@@ -21,7 +21,7 @@ interface UsePopUpReturn<T extends Readonly<string[]> | UsePopUpProps[]> {
|
||||
popUp: UsePopUpState<T>;
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<T>, data?: unknown) => void;
|
||||
handlePopUpClose: (popUpName: keyof UsePopUpState<T>) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<T>) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<T>, state?: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,10 +50,10 @@ export const usePopUp = <T extends Readonly<string[]> | UsePopUpProps[]>(
|
||||
setPopUp((oldState) => ({ ...oldState, [popUpName]: { isOpen: false } }));
|
||||
}, []);
|
||||
|
||||
const handlePopUpToggle = useCallback((popUpName: keyof UsePopUpState<T>) => {
|
||||
const handlePopUpToggle = useCallback((popUpName: keyof UsePopUpState<T>, state?: boolean) => {
|
||||
setPopUp((oldState) => ({
|
||||
...oldState,
|
||||
[popUpName]: { isOpen: !oldState[popUpName].isOpen }
|
||||
[popUpName]: { isOpen: typeof state === 'undefined' ? !oldState[popUpName].isOpen : state }
|
||||
}));
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ import Layout from '@app/components/basic/Layout';
|
||||
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
||||
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
||||
import { publicPaths } from '@app/const';
|
||||
import { SubscriptionProvider } from '@app/context';
|
||||
import { AuthProvider } from '@app/context/AuthContext';
|
||||
import { WorkspaceProvider } from '@app/context/WorkspaceContext';
|
||||
import { queryClient } from '@app/reactQuery';
|
||||
|
||||
import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||
@@ -68,11 +70,15 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<NotificationProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</NotificationProvider>
|
||||
<WorkspaceProvider>
|
||||
<SubscriptionProvider>
|
||||
<NotificationProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</NotificationProvider>
|
||||
</SubscriptionProvider>
|
||||
</WorkspaceProvider>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function Activity() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
|
||||
<div className="mx-6 lg:mx-0 w-full h-screen">
|
||||
<NavHeader pageName="Project Activity" isProjectRelated />
|
||||
{currentSidebarAction && (
|
||||
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface IMembershipOrg {
|
||||
organization: string;
|
||||
role: 'owner' | 'admin' | 'member';
|
||||
status: 'invited' | 'accepted';
|
||||
deniedPermissions: any[];
|
||||
}
|
||||
/**
|
||||
* This route lets us get all the users in an org.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This function change the access of a user in a certain organization
|
||||
* @param {string} organizationId
|
||||
* @param {string} membershipId
|
||||
* @param {string} role
|
||||
* @returns
|
||||
*/
|
||||
const changeUserRoleInOrganization = (organizationId: string, membershipId: string, role: string) =>
|
||||
SecurityClient.fetchCall(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
role
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res;
|
||||
}
|
||||
console.log('Failed to change the user role in an org');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default changeUserRoleInOrganization;
|
||||
@@ -0,0 +1,22 @@
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This route lets us get the environments that a certain user has acess to in a certain project
|
||||
* @param {string} workspaceId
|
||||
* @returns
|
||||
*/
|
||||
const getWorkspaceEnvironments = ({ workspaceId }: { workspaceId: string }) =>
|
||||
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res?.status === 200) {
|
||||
return (await res.json()).accessibleEnvironments;
|
||||
}
|
||||
console.log('Failed to get accessible environments');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default getWorkspaceEnvironments;
|
||||
@@ -41,11 +41,13 @@ import updateSecrets from '../api/files/UpdateSecrets';
|
||||
import getUser from '../api/user/getUser';
|
||||
import checkUserAction from '../api/userActions/checkUserAction';
|
||||
import registerUserAction from '../api/userActions/registerUserAction';
|
||||
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
|
||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||
|
||||
type WorkspaceEnv = {
|
||||
name: string;
|
||||
slug: string;
|
||||
isWriteDenied: boolean;
|
||||
};
|
||||
|
||||
interface SecretDataProps {
|
||||
@@ -134,7 +136,8 @@ export default function Dashboard() {
|
||||
const [selectedSnapshotEnv, setSelectedSnapshotEnv] = useState<WorkspaceEnv>();
|
||||
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>({
|
||||
name: '',
|
||||
slug: ''
|
||||
slug: '',
|
||||
isWriteDenied: false
|
||||
});
|
||||
const [atSecretAreaTop, setAtSecretsAreaTop] = useState(true);
|
||||
const secretsTop = useRef<HTMLDivElement>(null);
|
||||
@@ -216,9 +219,10 @@ export default function Dashboard() {
|
||||
}
|
||||
setAutoCapitalization(workspace?.autoCapitalization ?? true);
|
||||
|
||||
setWorkspaceEnvs(workspace?.environments || []);
|
||||
const accessibleEnvironments = await getWorkspaceEnvironments({ workspaceId });
|
||||
setWorkspaceEnvs(accessibleEnvironments || []);
|
||||
// set env
|
||||
const env = workspace?.environments?.[0] || {
|
||||
const env = accessibleEnvironments?.[0] || {
|
||||
name: 'unknown',
|
||||
slug: 'unkown'
|
||||
};
|
||||
@@ -353,6 +357,7 @@ export default function Dashboard() {
|
||||
findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
|
||||
|
||||
if (nameErrors) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'Solve all name errors before saving secrets.',
|
||||
type: 'error'
|
||||
@@ -360,12 +365,21 @@ export default function Dashboard() {
|
||||
}
|
||||
|
||||
if (duplicatesExist) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'Remove duplicated secret names before saving.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedEnv.isWriteDenied) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'You are not allowed to edit this environment',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
// Once "Save changes" is clicked, disable that button
|
||||
setButtonReady(false);
|
||||
|
||||
@@ -506,7 +520,7 @@ export default function Dashboard() {
|
||||
};
|
||||
|
||||
return data ? (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<div className="bg-bunker-800 max-h-screen h-full relative flex flex-col justify-between text-white">
|
||||
<Head>
|
||||
<title>{t('common:head-title', { title: t('dashboard:title') })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
@@ -514,7 +528,7 @@ export default function Dashboard() {
|
||||
<meta property="og:title" content={String(t('dashboard:og-title'))} />
|
||||
<meta name="og:description" content={String(t('dashboard:og-description'))} />
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row h-full">
|
||||
{sidebarSecretId !== 'None' && (
|
||||
<SideBar
|
||||
toggleSidebar={toggleSidebar}
|
||||
@@ -527,6 +541,9 @@ export default function Dashboard() {
|
||||
modifyValueOverride={listenChangeValueOverride}
|
||||
modifyComment={listenChangeComment}
|
||||
buttonReady={buttonReady}
|
||||
workspaceEnvs={workspaceEnvs}
|
||||
selectedEnv={selectedEnv}
|
||||
workspaceId={workspaceId}
|
||||
savePush={savePush}
|
||||
sharedToHide={sharedToHide}
|
||||
setSharedToHide={setSharedToHide}
|
||||
@@ -582,7 +599,8 @@ export default function Dashboard() {
|
||||
setSelectedEnv(
|
||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown'
|
||||
slug: 'unknown',
|
||||
isWriteDenied: false
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -662,7 +680,8 @@ export default function Dashboard() {
|
||||
setSelectedEnv(
|
||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown'
|
||||
slug: 'unknown',
|
||||
isWriteDenied: false
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -675,7 +694,8 @@ export default function Dashboard() {
|
||||
setSelectedSnapshotEnv(
|
||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown'
|
||||
slug: 'unknown',
|
||||
isWriteDenied: false
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -857,7 +877,6 @@ export default function Dashboard() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
|
||||
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full" />
|
||||
<Image src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -167,7 +167,7 @@ export default function Home() {
|
||||
})}
|
||||
{learningItem({
|
||||
text: 'Inject secrets locally',
|
||||
subText: 'Replace .env files with a more secure an efficient alternative.',
|
||||
subText: 'Replace .env files with a more secure and efficient alternative.',
|
||||
complete: false,
|
||||
icon: faNetworkWired,
|
||||
time: '8 min',
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function Login() {
|
||||
const [password, setPassword] = useState('');
|
||||
const [errorLogin, setErrorLogin] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const lang = router.locale ?? 'en';
|
||||
@@ -44,6 +45,7 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
if (isLoggedIn()) {
|
||||
setIsAlreadyLoggedIn(true);
|
||||
redirectToDashboard();
|
||||
}
|
||||
}, []);
|
||||
@@ -64,6 +66,10 @@ export default function Login() {
|
||||
});
|
||||
};
|
||||
|
||||
if (isAlreadyLoggedIn) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 h-screen flex flex-col justify-start px-6">
|
||||
<Head>
|
||||
|
||||
@@ -1,370 +1,29 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faCheck, faCopy, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '@app/components/basic/buttons/Button';
|
||||
import AddServiceTokenDialog from '@app/components/basic/dialog/AddServiceTokenDialog';
|
||||
import InputField from '@app/components/basic/InputField';
|
||||
import EnvironmentTable from '@app/components/basic/table/EnvironmentsTable';
|
||||
import ServiceTokenTable from '@app/components/basic/table/ServiceTokenTable';
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||
import { Checkbox } from '@app/components/v2';
|
||||
import deleteEnvironment from '@app/pages/api/environments/deleteEnvironment';
|
||||
import updateEnvironment from '@app/pages/api/environments/updateEnvironment';
|
||||
|
||||
import createEnvironment from '../../api/environments/createEnvironment';
|
||||
import getServiceTokens from '../../api/serviceToken/getServiceTokens';
|
||||
import deleteWorkspace from '../../api/workspace/deleteWorkspace';
|
||||
import getWorkspaces from '../../api/workspace/getWorkspaces';
|
||||
import renameWorkspace from '../../api/workspace/renameWorkspace';
|
||||
import toggleAutoCapitalization from '../../api/workspace/toggleAutoCapitalization';
|
||||
|
||||
type EnvData = {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export default function SettingsBasic() {
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const [autoCapitalizationButtonReady, setAutoCapitalizationButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
const [workspaceName, setWorkspaceName] = useState('');
|
||||
const [autoCapitalization, setAutoCapitalization] = useState(false);
|
||||
const [serviceTokens, setServiceTokens] = useState<any[]>([]);
|
||||
const [environments, setEnvironments] = useState<Array<EnvData>>([]);
|
||||
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
|
||||
// const [isAddOpen, setIsAddOpen] = useState(false);
|
||||
const [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] = useState(false);
|
||||
const [projectIdCopied, setProjectIdCopied] = useState(false);
|
||||
const workspaceId = router.query.id as string;
|
||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||
import { ProjectSettingsPage } from '@app/views/Settings/ProjectSettingsPage';
|
||||
|
||||
const ProjectSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
* This function copies the project id to the clipboard
|
||||
*/
|
||||
function copyToClipboard() {
|
||||
const copyText = document.getElementById('myInput') as HTMLInputElement;
|
||||
|
||||
if (copyText) {
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setProjectIdCopied(true);
|
||||
setTimeout(() => setProjectIdCopied(false), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
userWorkspaces.forEach((userWorkspace) => {
|
||||
if (userWorkspace._id === workspaceId) {
|
||||
setWorkspaceName(userWorkspace.name);
|
||||
setAutoCapitalization(userWorkspace.autoCapitalization ?? true);
|
||||
setEnvironments(userWorkspace.environments);
|
||||
}
|
||||
});
|
||||
const tempServiceTokens = await getServiceTokens({
|
||||
workspaceId
|
||||
});
|
||||
setServiceTokens(tempServiceTokens);
|
||||
};
|
||||
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const modifyWorkspaceName = (newName: string) => {
|
||||
setButtonReady(true);
|
||||
setWorkspaceName(newName);
|
||||
};
|
||||
|
||||
const toggleAutoCapitalizationCheckBox = (state: boolean) => {
|
||||
setAutoCapitalizationButtonReady(true);
|
||||
setAutoCapitalization(state);
|
||||
};
|
||||
|
||||
const submitChanges = (newWorkspaceName: string) => {
|
||||
renameWorkspace(workspaceId, newWorkspaceName);
|
||||
setButtonReady(false);
|
||||
};
|
||||
|
||||
const submitAutoCapitalizationChanges = () => {
|
||||
toggleAutoCapitalization(workspaceId, autoCapitalization);
|
||||
setAutoCapitalizationButtonReady(false);
|
||||
};
|
||||
|
||||
|
||||
const closeAddServiceTokenModal = () => {
|
||||
setIsAddServiceTokenDialogOpen(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onCreateEnvironment = async ({ name, slug }: EnvData) => {
|
||||
const res = await createEnvironment(workspaceId, {
|
||||
environmentName: name,
|
||||
environmentSlug: slug
|
||||
});
|
||||
if (res) {
|
||||
// TODO: on react-query migration do an api call to resync
|
||||
setEnvironments((env) => [...env, { name, slug }]);
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateEnvironment = async (oldSlug: string, { name, slug }: EnvData) => {
|
||||
const res = await updateEnvironment(workspaceId, {
|
||||
oldEnvironmentSlug: oldSlug,
|
||||
environmentName: name,
|
||||
environmentSlug: slug
|
||||
});
|
||||
// TODO: on react-query migration do an api call to resync
|
||||
if (res) {
|
||||
setEnvironments((env) => env.map((el) => (el.slug === oldSlug ? { name, slug } : el)));
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteEnvironment = async (slugToBeDelete: string) => {
|
||||
const res = await deleteEnvironment(workspaceId, slugToBeDelete);
|
||||
// TODO: on react-query migration do an api call to resync
|
||||
if (res) {
|
||||
setEnvironments((env) => env.filter(({ slug }) => slug !== slugToBeDelete));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<>
|
||||
<Head>
|
||||
<title>{t('common:head-title', { title: t('settings-project:title') })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<AddServiceTokenDialog
|
||||
isOpen={isAddServiceTokenDialogOpen}
|
||||
workspaceId={workspaceId}
|
||||
environments={environments}
|
||||
closeModal={closeAddServiceTokenModal}
|
||||
workspaceName={workspaceName}
|
||||
serviceTokens={serviceTokens}
|
||||
setServiceTokens={setServiceTokens}
|
||||
/>
|
||||
<div className="flex flex-row mr-6 max-w-5xl">
|
||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
||||
<NavHeader pageName={t('settings-project:title')} isProjectRelated />
|
||||
<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-project:title')}</p>
|
||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
||||
{t('settings-project:description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col ml-6 text-mineshaft-50">
|
||||
<div className="flex flex-col">
|
||||
<div className="min-w-md mt-2 flex flex-col items-start">
|
||||
<div className="bg-white/5 rounded-md px-6 pb-4 flex flex-col items-start w-full mb-6 pt-2">
|
||||
<p className="text-xl font-semibold mb-4 mt-2">{t('common:display-name')}</p>
|
||||
<div className="max-h-28 w-full max-w-md mr-auto">
|
||||
<InputField
|
||||
label=""
|
||||
onChangeHandler={modifyWorkspaceName}
|
||||
type="varName"
|
||||
value={workspaceName}
|
||||
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(workspaceName)}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
iconDisabled={faCheck}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pb-4 flex flex-col items-start w-full mb-6 pt-2">
|
||||
<p className="text-xl font-semibold mb-4 mt-2">{t('settings-project:auto-capitalization')}</p>
|
||||
<Checkbox
|
||||
className='data-[state=checked]:bg-primary'
|
||||
id='autoCapitalization'
|
||||
isChecked={autoCapitalization}
|
||||
onCheckedChange={(state) => toggleAutoCapitalizationCheckBox(state as boolean)}
|
||||
> {t('settings-project:auto-capitalization-description')}</Checkbox>
|
||||
<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={() => submitAutoCapitalizationChanges()}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={autoCapitalizationButtonReady}
|
||||
iconDisabled={faCheck}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start w-full mb-6 mt-4">
|
||||
<p className="text-xl font-semibold self-start">{t('common:project-id')}</p>
|
||||
<p className="text-base text-gray-400 font-normal self-start mt-4">
|
||||
{t('settings-project:project-id-description')}
|
||||
</p>
|
||||
<p className="text-base text-gray-400 font-normal self-start">
|
||||
{t('settings-project:project-id-description2')}
|
||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a
|
||||
href="https://infisical.com/docs/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary hover:opacity-80 duration-200"
|
||||
>
|
||||
{t('settings-project:docs')}
|
||||
</a>
|
||||
</p>
|
||||
<p className="mt-4 text-xs text-bunker-300">
|
||||
{t('settings-project:auto-generated')}
|
||||
</p>
|
||||
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mb-3 mr-2 rounded-md text-gray-400">
|
||||
<p className="mr-2 font-bold pl-4">{`${t('common:project-id')}:`}</p>
|
||||
<input
|
||||
type="text"
|
||||
value={workspaceId}
|
||||
id="myInput"
|
||||
className="bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none"
|
||||
disabled
|
||||
/>
|
||||
<div className="group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyToClipboard}
|
||||
className="pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
|
||||
>
|
||||
{projectIdCopied ? (
|
||||
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
)}
|
||||
</button>
|
||||
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-bunker-800 rounded-md text-center text-gray-400 text-sm">
|
||||
{t('common:click-to-copy')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start w-full mt-4 mb-4">
|
||||
<EnvironmentTable
|
||||
data={environments}
|
||||
onCreateEnv={onCreateEnvironment}
|
||||
onUpdateEnv={onUpdateEnvironment}
|
||||
onDeleteEnv={onDeleteEnvironment}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-4 mb-4 pt-2">
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div className="flex flex-col w-full">
|
||||
<p className="text-xl font-semibold mb-3">
|
||||
{t('section-token:service-tokens')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{t('section-token:service-tokens-description')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400 mb-4">
|
||||
Please, make sure you are on the
|
||||
<a
|
||||
className="text-primary underline underline-offset-2 ml-1"
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
latest version of CLI
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-48 mt-2">
|
||||
<Button
|
||||
text={t('section-token:add-new') as string}
|
||||
onButtonPressed={() => {
|
||||
setIsAddServiceTokenDialogOpen(true);
|
||||
}}
|
||||
color="mineshaft"
|
||||
icon={faPlus}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ServiceTokenTable
|
||||
data={serviceTokens}
|
||||
workspaceName={workspaceName}
|
||||
setServiceTokens={setServiceTokens as any}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 border-l border-red pl-6 flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2">
|
||||
<p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p>
|
||||
<p className="mt-2 text-md text-gray-400">{t('settings-project:danger-zone-note')}</p>
|
||||
<div className="max-h-28 w-full max-w-md mr-auto mt-4">
|
||||
<InputField
|
||||
label={t('settings-project:project-to-delete') as string}
|
||||
onChangeHandler={setWorkspaceToBeDeletedName}
|
||||
type="varName"
|
||||
value={workspaceToBeDeletedName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="max-w-md mt-6 w-full 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-semibold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={executeDeletingWorkspace}
|
||||
>
|
||||
{t('settings-project:delete-project')}
|
||||
</button>
|
||||
<p className="mt-0.5 ml-1 text-xs text-gray-500">
|
||||
{t('settings-project:delete-project-note')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProjectSettingsPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SettingsBasic.requireAuth = true;
|
||||
export default ProjectSettings;
|
||||
|
||||
export const getServerSideProps = getTranslatedServerSideProps([
|
||||
ProjectSettings.requireAuth = true;
|
||||
|
||||
export const getServerSideProps = getTranslatedStaticProps([
|
||||
'settings',
|
||||
'settings-project',
|
||||
'section-token'
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '@app/components/basic/buttons/Button';
|
||||
import AddProjectMemberDialog from '@app/components/basic/dialog/AddProjectMemberDialog';
|
||||
import UserTable from '@app/components/basic/table/UserTable';
|
||||
import ProjectUsersTable from '@app/components/basic/table/ProjectUsersTable';
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
import guidGenerator from '@app/components/utilities/randomId';
|
||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||
@@ -33,6 +33,7 @@ interface UserProps {
|
||||
}
|
||||
|
||||
interface MembershipProps {
|
||||
deniedPermissions: any[];
|
||||
user: UserProps;
|
||||
inviteEmail: string;
|
||||
role: string;
|
||||
@@ -76,6 +77,7 @@ export default function Users() {
|
||||
status: membership?.status,
|
||||
userId: membership.user?._id,
|
||||
membershipId: membership._id,
|
||||
deniedPermissions: membership.deniedPermissions,
|
||||
publicKey: membership.user?.publicKey
|
||||
}));
|
||||
setUserList(tempUserList);
|
||||
@@ -144,7 +146,7 @@ export default function Users() {
|
||||
};
|
||||
|
||||
return userList ? (
|
||||
<div className="bg-bunker-800 md:h-screen flex flex-col justify-start">
|
||||
<div className="bg-bunker-800 md:h-screen flex flex-col justify-start max-w-[calc(100vw-240px)]">
|
||||
<Head>
|
||||
<title>{t('common:head-title', { title: t('settings-members:title') })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
@@ -168,7 +170,7 @@ export default function Users() {
|
||||
setEmail={setEmail}
|
||||
/>
|
||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
||||
<div className="px-6 pb-1 w-full flex flex-row items-start min-w-6xl max-w-6xl">
|
||||
<div className="px-6 pb-1 w-full flex flex-row items-start">
|
||||
<div className="h-10 w-full bg-white/5 mt-2 rounded-md flex flex-row items-center">
|
||||
<FontAwesomeIcon
|
||||
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
|
||||
@@ -191,14 +193,12 @@ export default function Users() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="block overflow-y-auto min-w-6xl max-w-6xl px-6">
|
||||
<UserTable
|
||||
<div className="block overflow-x-scroll px-6 pb-6 no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
<ProjectUsersTable
|
||||
userData={userList}
|
||||
changeData={setUserList}
|
||||
myUser={personalEmail}
|
||||
filter={searchUsers}
|
||||
resendInvite={submitAddModal}
|
||||
isOrg={false}
|
||||
// onClick={openDeleteModal}
|
||||
// deleteUser={deleteMembership}
|
||||
// setUserIdToBeDeleted={setUserIdToBeDeleted}
|
||||
@@ -207,7 +207,6 @@ export default function Users() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
|
||||
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full" />
|
||||
<Image src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function VerifyEmail() {
|
||||
<title>Login</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content="Log In to Infisical" />
|
||||
<meta property="og:title" content="Verify your email in Infisical" />
|
||||
<meta
|
||||
name="og:description"
|
||||
content="Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
// TODO(akhilmhdh):Refactor this into a better utility module package
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptSymmetric
|
||||
} from '@app/components/utilities/cryptography/crypto';
|
||||
import { Button, FormControl, Input } from '@app/components/v2';
|
||||
import { plans } from '@app/const';
|
||||
import { useSubscription, useWorkspace } from '@app/context';
|
||||
import { useToggle } from '@app/hooks';
|
||||
import {
|
||||
useCreateServiceToken,
|
||||
useCreateWsEnvironment,
|
||||
useDeleteServiceToken,
|
||||
useDeleteWorkspace,
|
||||
useDeleteWsEnvironment,
|
||||
useGetUserWsKey,
|
||||
useGetUserWsServiceTokens,
|
||||
useRenameWorkspace,
|
||||
useUpdateWsEnvironment
|
||||
} from '@app/hooks/api';
|
||||
|
||||
import {
|
||||
CopyProjectIDSection,
|
||||
CreateServiceToken,
|
||||
CreateUpdateEnvFormData,
|
||||
EnvironmentSection,
|
||||
ProjectNameChangeSection,
|
||||
ServiceTokenSection
|
||||
} from './components';
|
||||
|
||||
export const ProjectSettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentWorkspace, workspaces } = useWorkspace();
|
||||
const router = useRouter();
|
||||
const { data: serviceTokens } = useGetUserWsServiceTokens({
|
||||
workspaceID: currentWorkspace?._id || ''
|
||||
});
|
||||
const workspaceID = currentWorkspace?._id || '';
|
||||
const { createNotification } = useNotificationContext();
|
||||
// delete action worksapce
|
||||
const [deleteProjectInput, setDeleteProjectInput] = useState('');
|
||||
const [isDeleting, setIsDeleting] = useToggle();
|
||||
|
||||
const renameWorkspace = useRenameWorkspace();
|
||||
const deleteWorkspace = useDeleteWorkspace();
|
||||
// env crud operation
|
||||
const createWsEnv = useCreateWsEnvironment();
|
||||
const updateWsEnv = useUpdateWsEnvironment();
|
||||
const deleteWsEnv = useDeleteWsEnvironment();
|
||||
|
||||
// service token
|
||||
const { data: latestFileKey } = useGetUserWsKey(workspaceID);
|
||||
const createServiceToken = useCreateServiceToken();
|
||||
const deleteServiceToken = useDeleteServiceToken();
|
||||
|
||||
// get user subscription
|
||||
const { subscriptionPlan } = useSubscription();
|
||||
const host = window.location.origin;
|
||||
const isEnvServiceAllowed =
|
||||
subscriptionPlan !== plans.starter || host !== 'https://app.infisical.com';
|
||||
|
||||
const onRenameWorkspace = async (name: string) => {
|
||||
try {
|
||||
await renameWorkspace.mutateAsync({ workspaceID, newWorkspaceName: name });
|
||||
createNotification({
|
||||
text: 'Successfully renamed workspace',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: 'Failed to rename workspace',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteWorkspace = async () => {
|
||||
setIsDeleting.on();
|
||||
try {
|
||||
await deleteWorkspace.mutateAsync({ workspaceID });
|
||||
// redirect user to first workspace user is part of
|
||||
const ws = workspaces.find(({ _id }) => _id !== workspaceID);
|
||||
router.push(`/dashboard/${ws?._id}`);
|
||||
createNotification({
|
||||
text: 'Successfully deleted workspace',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: 'Failed to delete workspace',
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
setIsDeleting.off();
|
||||
}
|
||||
};
|
||||
|
||||
// workspace environment operation
|
||||
const onCreateWsEnv = async ({ environmentName, environmentSlug }: CreateUpdateEnvFormData) => {
|
||||
try {
|
||||
await createWsEnv.mutateAsync({ workspaceID, environmentName, environmentSlug });
|
||||
createNotification({
|
||||
text: 'Successfully created environment',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: 'Failed to create environment',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateWsEnv = async (
|
||||
oldEnvironmentSlug: string,
|
||||
{ environmentName, environmentSlug }: CreateUpdateEnvFormData
|
||||
) => {
|
||||
try {
|
||||
await updateWsEnv.mutateAsync({
|
||||
workspaceID,
|
||||
environmentName,
|
||||
environmentSlug,
|
||||
oldEnvironmentSlug
|
||||
});
|
||||
createNotification({
|
||||
text: 'Successfully updated environment',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: 'Failed to update environment',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteWsEnv = async (environmentSlug: string) => {
|
||||
try {
|
||||
await deleteWsEnv.mutateAsync({
|
||||
workspaceID,
|
||||
environmentSlug
|
||||
});
|
||||
createNotification({
|
||||
text: 'Successfully deleted environment',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: 'Failed to delete environment',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onCreateServiceToken = async ({ environment, expiresIn, name }: CreateServiceToken) => {
|
||||
// type guard
|
||||
if (!latestFileKey) return '';
|
||||
try {
|
||||
// crypo calculation to generate the key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestFileKey.encryptedKey,
|
||||
nonce: latestFileKey.nonce,
|
||||
publicKey: latestFileKey.sender.publicKey,
|
||||
privateKey: localStorage.getItem('PRIVATE_KEY') as string
|
||||
});
|
||||
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: key,
|
||||
key: randomBytes
|
||||
});
|
||||
|
||||
const res = await createServiceToken.mutateAsync({
|
||||
encryptedKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
environment,
|
||||
expiresIn: Number(expiresIn),
|
||||
name,
|
||||
workspaceId: workspaceID
|
||||
});
|
||||
console.log(res);
|
||||
createNotification({
|
||||
text: 'Successfully created a service token',
|
||||
type: 'success'
|
||||
});
|
||||
return res.serviceToken;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: 'Failed to create a service token',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const onDeleteServiceToken = async (tokenID: string) => {
|
||||
try {
|
||||
await deleteServiceToken.mutateAsync(tokenID);
|
||||
createNotification({
|
||||
text: 'Successfully revoked service token',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: 'Failed to delete service token',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col px-8 text-mineshaft-50">
|
||||
{/* TODO(akhilmhdh): Remove this right when layout is refactored */}
|
||||
<div className="relative right-5">
|
||||
<NavHeader pageName={t('settings-project:title')} isProjectRelated />
|
||||
</div>
|
||||
<div className="my-8 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-project:title')}</p>
|
||||
<p className="mr-4 text-base font-normal text-gray-400">
|
||||
{t('settings-project:description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ProjectNameChangeSection
|
||||
workspaceName={currentWorkspace?.name}
|
||||
onProjectNameChange={onRenameWorkspace}
|
||||
/>
|
||||
<CopyProjectIDSection workspaceID={currentWorkspace?._id || ''} />
|
||||
<EnvironmentSection
|
||||
environments={currentWorkspace?.environments || []}
|
||||
onCreate={onCreateWsEnv}
|
||||
onDelete={onDeleteWsEnv}
|
||||
onUpdate={onUpdateWsEnv}
|
||||
isEnvServiceAllowed={isEnvServiceAllowed}
|
||||
/>
|
||||
<ServiceTokenSection
|
||||
tokens={serviceTokens || []}
|
||||
environments={currentWorkspace?.environments || []}
|
||||
onDeleteToken={onDeleteServiceToken}
|
||||
workspaceName={currentWorkspace?.name || ''}
|
||||
onCreateToken={onCreateServiceToken}
|
||||
/>
|
||||
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-white/5 px-6 pl-6 pb-4 pt-4">
|
||||
<p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p>
|
||||
<p className="text-md mt-2 text-gray-400">{t('settings-project:danger-zone-note')}</p>
|
||||
<div className="mr-auto mt-4 max-h-28 w-full max-w-md">
|
||||
<FormControl
|
||||
label={
|
||||
<div className="mb-0.5 text-sm font-normal text-gray-400">
|
||||
Type <span className="font-bold">{currentWorkspace?.name}</span> to delete the
|
||||
workspace
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
onChange={(e) => setDeleteProjectInput(e.target.value)}
|
||||
value={deleteProjectInput}
|
||||
placeholder="Type the project name to delete"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
onClick={onDeleteWorkspace}
|
||||
isDisabled={deleteProjectInput !== currentWorkspace?.name || isDeleting}
|
||||
isLoading={isDeleting}
|
||||
>
|
||||
{t('settings-project:delete-project')}
|
||||
</Button>
|
||||
<p className="mt-3 ml-0.5 text-xs text-gray-500">
|
||||
{t('settings-project:delete-project-note')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { IconButton } from '@app/components/v2';
|
||||
import { useToggle } from '@app/hooks';
|
||||
|
||||
type Props = {
|
||||
workspaceID: string;
|
||||
};
|
||||
|
||||
export const CopyProjectIDSection = ({ workspaceID }: Props): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isProjectIdCopied) {
|
||||
timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [isProjectIdCopied]);
|
||||
|
||||
const copyProjectIdToClipboard = () => {
|
||||
navigator.clipboard.writeText(workspaceID);
|
||||
setIsProjectIdCopied.on();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-4 pb-2">
|
||||
<p className="self-start text-xl font-semibold">{t('common:project-id')}</p>
|
||||
<p className="mt-4 self-start text-base font-normal text-gray-400">
|
||||
{t('settings-project:project-id-description')}
|
||||
</p>
|
||||
<p className="mt-2 self-start text-base font-normal text-gray-400">
|
||||
{t('settings-project:project-id-description2')}
|
||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a
|
||||
href="https://infisical.com/docs/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary duration-200 hover:opacity-80"
|
||||
>
|
||||
{t('settings-project:docs')}
|
||||
</a>
|
||||
</p>
|
||||
<p className="mt-4 text-xs text-bunker-300">{t('settings-project:auto-generated')}</p>
|
||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400">
|
||||
<p className="mr-2 pl-4 font-bold">{`${t('common:project-id')}:`}</p>
|
||||
<p className="mr-4">{workspaceID}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => copyProjectIdToClipboard()}
|
||||
>
|
||||
<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{t('common:click-to-copy')}
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { CopyProjectIDSection } from './CopyProjectIDSection';
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { faPencil, faPlus, faTrashCan } 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,
|
||||
UpgradePlanModal
|
||||
} from '@app/components/v2';
|
||||
import { usePopUp } from '@app/hooks/usePopUp';
|
||||
|
||||
type Props = {
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
isEnvServiceAllowed: boolean;
|
||||
onCreate: (data: CreateUpdateEnvFormData) => Promise<void>;
|
||||
onUpdate: (oldEnvSlug: string, data: CreateUpdateEnvFormData) => Promise<void>;
|
||||
onDelete: (envSlug: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const createUpdateEnvSchema = yup.object({
|
||||
environmentName: yup.string().label('Environment Name').required(),
|
||||
environmentSlug: yup.string().label('Environment Slug').required()
|
||||
});
|
||||
|
||||
export type CreateUpdateEnvFormData = yup.InferType<typeof createUpdateEnvSchema>;
|
||||
|
||||
export const EnvironmentSection = ({
|
||||
environments,
|
||||
isEnvServiceAllowed,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onUpdate
|
||||
}: Props): JSX.Element => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
'createUpdateEnv',
|
||||
'deleteEnv',
|
||||
'upgradePlan'
|
||||
] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<CreateUpdateEnvFormData>({
|
||||
resolver: yupResolver(createUpdateEnvSchema)
|
||||
});
|
||||
|
||||
const isEnvUpdate = Boolean(popUp?.createUpdateEnv?.data);
|
||||
const oldEnvSlug = (popUp?.createUpdateEnv?.data as { slug: string })?.slug;
|
||||
|
||||
const onEnvModalSubmit = async (data: CreateUpdateEnvFormData) => {
|
||||
if (isEnvUpdate) {
|
||||
await onUpdate(oldEnvSlug, data);
|
||||
} else {
|
||||
await onCreate(data);
|
||||
}
|
||||
handlePopUpClose('createUpdateEnv');
|
||||
};
|
||||
|
||||
const onEnvDeleteSubmit = async (envSlug: string) => {
|
||||
await onDelete(envSlug);
|
||||
handlePopUpClose('deleteEnv');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-white/5 p-6">
|
||||
<div className="mb-2 flex w-full flex-row justify-between">
|
||||
<div className="flex w-full flex-col">
|
||||
<p className="mb-3 text-xl font-semibold">Project Environments</p>
|
||||
<p className="mb-4 text-base text-gray-400">
|
||||
Choose which environments will show up in your dashboard like development, staging,
|
||||
production
|
||||
</p>
|
||||
<p className="mr-1 self-start text-sm text-gray-500">
|
||||
Note: the text in slugs shows how these environmant should be accessed in CLI.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (isEnvServiceAllowed) {
|
||||
handlePopUpOpen('createUpdateEnv');
|
||||
} else {
|
||||
handlePopUpOpen('upgradePlan');
|
||||
}
|
||||
}}
|
||||
colorSchema="primary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add New Env
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Slug</Th>
|
||||
<Th aria-label="button" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{environments?.length > 0 ? (
|
||||
environments.map(({ name, slug }) => (
|
||||
<Tr key={name}>
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td className="flex items-center justify-end">
|
||||
<IconButton
|
||||
className="mr-3"
|
||||
onClick={() => {
|
||||
if (isEnvServiceAllowed) {
|
||||
handlePopUpOpen('createUpdateEnv', { name, slug });
|
||||
reset({ environmentName: name, environmentSlug: slug });
|
||||
} else {
|
||||
handlePopUpOpen('upgradePlan');
|
||||
}
|
||||
}}
|
||||
colorSchema="secondary"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (isEnvServiceAllowed) {
|
||||
handlePopUpOpen('deleteEnv', { name, slug });
|
||||
} else {
|
||||
handlePopUpOpen('upgradePlan');
|
||||
}
|
||||
}}
|
||||
colorSchema="danger"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
))
|
||||
) : (
|
||||
<Tr>
|
||||
<Td colSpan={4} className="pt-7 pb-5 text-center text-bunker-400">
|
||||
No environments found
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Modal
|
||||
isOpen={popUp?.createUpdateEnv?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle('createUpdateEnv', isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title={isEnvUpdate ? 'Update environment' : 'Create a new environment'}>
|
||||
<form onSubmit={handleSubmit(onEnvModalSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="environmentName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="environmentSlug"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment Slug"
|
||||
helperText="Slugs are shorthands used in cli to access environment"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isEnvUpdate ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteEnv.isOpen}
|
||||
title={`Are you sure want to delete ${
|
||||
(popUp?.deleteEnv?.data as { name: string })?.name || ' '
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle('deleteEnv', isOpen)}
|
||||
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''}
|
||||
onDeleteApproved={() =>
|
||||
onEnvDeleteSubmit((popUp?.deleteEnv?.data as { slug: string })?.slug)
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { EnvironmentSection } from './EnvironmentSection';
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
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 = {
|
||||
workspaceName?: string;
|
||||
onProjectNameChange: (name: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label('Project Name')
|
||||
});
|
||||
|
||||
type FormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const ProjectNameChangeSection = ({
|
||||
workspaceName,
|
||||
onProjectNameChange
|
||||
}: Props): JSX.Element => {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
formState: { isDirty, isSubmitting }
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
reset({ name: workspaceName });
|
||||
}, [workspaceName]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
await onProjectNameChange(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 project 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>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { ProjectNameChangeSection } from './ProjectNameChangeSection';
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { faCheck, faCopy, faPlus, faTrashCan } 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,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalTrigger,
|
||||
Select,
|
||||
SelectItem,
|
||||
Table,
|
||||
TableContainer,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from '@app/components/v2';
|
||||
import { usePopUp, useToggle } from '@app/hooks';
|
||||
import { ServiceToken, WorkspaceEnv } from '@app/hooks/api/types';
|
||||
|
||||
const apiTokenExpiry = [
|
||||
{ label: '1 Day', value: 86400 },
|
||||
{ label: '7 Days', value: 604800 },
|
||||
{ label: '1 Month', value: 2592000 },
|
||||
{ label: '6 months', value: 15552000 },
|
||||
{ label: '12 months', value: 31104000 }
|
||||
];
|
||||
|
||||
const createServiceTokenSchema = yup.object({
|
||||
name: yup.string().required().label('Service Token Name'),
|
||||
environment: yup.string().required().label('Environment'),
|
||||
expiresIn: yup.string().required().label('Service Token Name')
|
||||
});
|
||||
|
||||
export type CreateServiceToken = yup.InferType<typeof createServiceTokenSchema>;
|
||||
|
||||
type Props = {
|
||||
tokens: ServiceToken[];
|
||||
workspaceName: string;
|
||||
environments: WorkspaceEnv[];
|
||||
onDeleteToken: (serviceTokenID: string) => Promise<void>;
|
||||
onCreateToken: (data: CreateServiceToken) => Promise<string>;
|
||||
};
|
||||
|
||||
type DeleteModalData = { name: string; id: string };
|
||||
|
||||
export const ServiceTokenSection = ({
|
||||
tokens = [],
|
||||
onDeleteToken,
|
||||
workspaceName,
|
||||
environments = [],
|
||||
onCreateToken
|
||||
}: Props): JSX.Element => {
|
||||
const [newToken, setToken] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isTokenCopied) {
|
||||
timer = setTimeout(() => setIsTokenCopied.off(), 2000);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [isTokenCopied]);
|
||||
|
||||
const copyTokenToClipboard = () => {
|
||||
navigator.clipboard.writeText(newToken);
|
||||
setIsTokenCopied.on();
|
||||
};
|
||||
|
||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
'createAPIToken',
|
||||
'deleteAPITokenConfirmation'
|
||||
] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<CreateServiceToken>({
|
||||
resolver: yupResolver(createServiceTokenSchema)
|
||||
});
|
||||
|
||||
const hasServiceToken = Boolean(newToken);
|
||||
|
||||
const onFormSubmit = async (data: CreateServiceToken) => {
|
||||
const token = await onCreateToken(data);
|
||||
setToken(token);
|
||||
};
|
||||
|
||||
const onDeleteApproved = async () => {
|
||||
await onDeleteToken((popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.id);
|
||||
handlePopUpClose('deleteAPITokenConfirmation');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 mb-4 flex w-full flex-col items-start rounded-md bg-white/5 p-6">
|
||||
<div className="flex w-full flex-row justify-between">
|
||||
<div className="flex w-full flex-col">
|
||||
<p className="mb-3 text-xl font-semibold">{t('section-token:service-tokens')}</p>
|
||||
<p className="text-sm text-gray-400">{t('section-token:service-tokens-description')}</p>
|
||||
<p className="mb-4 text-sm text-gray-400">
|
||||
Please, make sure you are on the
|
||||
<a
|
||||
className="ml-1 text-primary underline underline-offset-2"
|
||||
href="https://infisical.com/docs/cli/overview"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
latest version of CLI
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Modal
|
||||
isOpen={popUp?.createAPIToken?.isOpen}
|
||||
onOpenChange={(open) => {
|
||||
handlePopUpToggle('createAPIToken', open);
|
||||
reset();
|
||||
setToken('');
|
||||
}}
|
||||
>
|
||||
<ModalTrigger asChild>
|
||||
<Button color="mineshaft" leftIcon={<FontAwesomeIcon icon={faPlus} />}>
|
||||
{t('section-token:add-new')}
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
title={
|
||||
t('section-token:add-dialog.title', {
|
||||
target: workspaceName
|
||||
}) as string
|
||||
}
|
||||
subTitle={t('section-token:add-dialog.description') as string}
|
||||
>
|
||||
{!hasServiceToken ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={t('section-token:add-dialog.name')}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Type your token name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{environments.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
defaultValue={String(apiTokenExpiry?.[0]?.value)}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Token Expiry"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{apiTokenExpiry.map(({ label, value }) => (
|
||||
<SelectItem value={String(value)} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||
<p className="mr-4 break-all">{newToken}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeIcon icon={isTokenCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{t('common:click-to-copy')}
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteAPITokenConfirmation.isOpen}
|
||||
title={`Delete ${
|
||||
(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name || ' '
|
||||
} api key?`}
|
||||
onChange={(isOpen) => handlePopUpToggle('deleteAPITokenConfirmation', isOpen)}
|
||||
deleteKey={(popUp?.deleteAPITokenConfirmation?.data as DeleteModalData)?.name}
|
||||
onClose={() => handlePopUpClose('deleteAPITokenConfirmation')}
|
||||
onDeleteApproved={onDeleteApproved}
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Token Name</Th>
|
||||
<Th>Environment</Th>
|
||||
<Th>Valid Until</Th>
|
||||
<Th aria-label="button" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{tokens?.length > 0 ? (
|
||||
tokens.map((row) => (
|
||||
<Tr key={row._id}>
|
||||
<Td>{row.name}</Td>
|
||||
<Td>{row.environment}</Td>
|
||||
<Td>{new Date(row.expiresAt).toUTCString()}</Td>
|
||||
<Td className="flex items-center justify-end">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
handlePopUpOpen('deleteAPITokenConfirmation', {
|
||||
name: row.name,
|
||||
id: row._id
|
||||
})
|
||||
}
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashCan} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
))
|
||||
) : (
|
||||
<Tr>
|
||||
<Td colSpan={4} className="py-6 text-center text-bunker-400">
|
||||
No service tokens found
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {ServiceTokenSection} from './ServiceTokenSection'
|
||||
@@ -0,0 +1,6 @@
|
||||
export { CopyProjectIDSection } from './CopyProjectIDSection';
|
||||
export { EnvironmentSection } from './EnvironmentSection';
|
||||
export type { CreateUpdateEnvFormData } from './EnvironmentSection/EnvironmentSection';
|
||||
export { ProjectNameChangeSection } from './ProjectNameChangeSection';
|
||||
export { ServiceTokenSection } from './ServiceTokenSection';
|
||||
export type { CreateServiceToken } from './ServiceTokenSection/ServiceTokenSection';
|
||||
@@ -0,0 +1 @@
|
||||
export { ProjectSettingsPage } from './ProjectSettingsPage';
|
||||
Reference in New Issue
Block a user